From 986af6c62ceef72ee6f5d0c4148aa4546679fb56 Mon Sep 17 00:00:00 2001 From: zachwick Date: Mon, 14 Mar 2016 13:54:21 -0400 Subject: [PATCH 1/4] Adds docstrings to Adafruit_Thermal class Signed-off-by: zachwick Fixes whitespace Signed-off-by: zachwick Fixes whitespace Signed-off-by: zachwick Removes the 'add docstring' TODO as it is done Signed-off-by: zachwick --- Adafruit_Thermal.py | 1214 ++++++++++++++++++++++++------------------- 1 file changed, 688 insertions(+), 526 deletions(-) diff --git a/Adafruit_Thermal.py b/Adafruit_Thermal.py index f5e5162..e4e316a 100644 --- a/Adafruit_Thermal.py +++ b/Adafruit_Thermal.py @@ -30,7 +30,6 @@ # settings in a global configuration file (rather than in the library). # - Make this use proper Python library installation procedure. # - Trap errors properly. Some stuff just falls through right now. -# - Add docstrings throughout! # Python 2.X code using the library usu. needs to include the next line: from __future__ import print_function @@ -38,536 +37,699 @@ import time class Adafruit_Thermal(Serial): + """Main class for interacting with a Adafruit Thermal Printer.""" + + resumeTime = 0.0 + byteTime = 0.0 + dotPrintTime = 0.033 + dotFeedTime = 0.0025 + prevByte = '\n' + column = 0 + maxColumn = 32 + charHeight = 24 + lineSpacing = 8 + barcodeHeight = 50 + printMode = 0 + defaultHeatTime = 60 + + def __init__(self, *args, **kwargs): + """Create a new connection using a given port and baud rate. + +The default port is `/dev/ttyAMA0` and the default baud rate is +19200. If you only pass in one of these parameters, then the +other parameter will use its default value.""" + + # If no parameters given, use default port & baud rate. + # If only port is passed, use default baud rate. + # If both passed, use those values. + baudrate = 19200 + if len(args) == 0: + args = [ "/dev/ttyAMA0", baudrate ] + elif len(args) == 1: + args = [ args[0], baudrate ] + else: + baudrate = args[1] + + # Calculate time to issue one byte to the printer. + # 11 bits (not 8) to accommodate idle, start and stop bits. + # Idle time might be unnecessary, but erring on side of + # caution here. + self.byteTime = 11.0 / float(baudrate) + + Serial.__init__(self, *args, **kwargs) + + # Remainder of this method was previously in begin() + + # The printer can't start receiving data immediately upon + # power up -- it needs a moment to cold boot and initialize. + # Allow at least 1/2 sec of uptime before printer can + # receive data. + self.timeoutSet(0.5) + + self.wake() + self.reset() + + # Description of print settings from page 23 of the manual: + # ESC 7 n1 n2 n3 Setting Control Parameter Command + # Decimal: 27 55 n1 n2 n3 + # Set "max heating dots", "heating time", "heating interval" + # n1 = 0-255 Max heat dots, Unit (8dots), Default: 7 (64 dots) + # n2 = 3-255 Heating time, Unit (10us), Default: 80 (800us) + # n3 = 0-255 Heating interval, Unit (10us), Default: 2 (20us) + # The more max heating dots, the more peak current will cost + # when printing, the faster printing speed. The max heating + # dots is 8*(n1+1). The more heating time, the more density, + # but the slower printing speed. If heating time is too short, + # blank page may occur. The more heating interval, the more + # clear, but the slower printing speed. + + heatTime = kwargs.get('heattime', self.defaultHeatTime) + self.writeBytes( + 27, # Esc + 55, # 7 (print settings) + 20, # Heat dots (20 = balance darkness w/no jams) + heatTime, # Lib default = 45 + 250) # Heat interval (500 uS = slower but darker) + + # Description of print density from page 23 of the manual: + # DC2 # n Set printing density + # Decimal: 18 35 n + # D4..D0 of n is used to set the printing density. + # Density is 50% + 5% * n(D4-D0) printing density. + # D7..D5 of n is used to set the printing break time. + # Break time is n(D7-D5)*250us. + # (Unsure of the default value for either -- not documented) + + printDensity = 14 # 120% (can go higher, but text gets fuzzy) + printBreakTime = 4 # 500 uS + + self.writeBytes( + 18, # DC2 + 35, # Print density + (printBreakTime << 5) | printDensity) + + self.dotPrintTime = 0.03 + self.dotFeedTime = 0.0021 + + # Because there's no flow control between the printer and computer, + # special care must be taken to avoid overrunning the printer's + # buffer. Serial output is throttled based on serial speed as well + # as an estimate of the device's print and feed rates (relatively + # slow, being bound to moving parts and physical reality). After + # an operation is issued to the printer (e.g. bitmap print), a + # timeout is set before which any other printer operations will be + # suspended. This is generally more efficient than using a delay + # in that it allows the calling code to continue with other duties + # (e.g. receiving or decoding an image) while the printer + # physically completes the task. + + # Sets estimated completion time for a just-issued task. + def timeoutSet(self, x): + """Sets the estimates completion time for a just-issued task. + +This is used to ensure that the python script does not overrun +the printer's buffer. Using this method instead of a delay +allows the code calling this method to continue executing +instead of blocking on the print.""" + + self.resumeTime = time.time() + x + + # Waits (if necessary) for the prior task to complete. + def timeoutWait(self): + """Waits (if neccessary) for the prior task to complete.""" + + while (time.time() - self.resumeTime) < 0: pass + + + # Printer performance may vary based on the power supply voltage, + # thickness of paper, phase of the moon and other seemingly random + # variables. This method sets the times (in microseconds) for the + # paper to advance one vertical 'dot' when printing and feeding. + # For example, in the default initialized state, normal-sized text + # is 24 dots tall and the line spacing is 32 dots, so the time for + # one line to be issued is approximately 24 * print time + 8 * feed + # time. The default print and feed times are based on a random + # test unit, but as stated above your reality may be influenced by + # many factors. This lets you tweak the timing to avoid excessive + # delays and/or overrunning the printer buffer. + def setTimes(self, p, f): + """Sets the time (in microseconds) for the paper to advance one +vertical 'dot' when printing and feeding. + +This rate varies based on the power supply voltage, thickness of +paper, phase of the moon, and other seemingly random variables. +As such, they may need tweaked for your individual printer.""" + + # Units are in microseconds for + # compatibility with Arduino library + self.dotPrintTime = p / 1000000.0 + self.dotFeedTime = f / 1000000.0 + + + # 'Raw' byte-writing method + def writeBytes(self, *args): + """Writes 'raw' bytes.""" + + self.timeoutWait() + self.timeoutSet(len(args) * self.byteTime) + for arg in args: + super(Adafruit_Thermal, self).write(chr(arg)) + + # Override write() method to keep track of paper feed. + def write(self, *data): + """Overrides the `write()` method to be able to keep track of +paper feeding.""" + + for i in range(len(data)): + c = data[i] + if c != 0x13: + self.timeoutWait() + super(Adafruit_Thermal, self).write(c) + d = self.byteTime + if ((c == '\n') or + (self.column == self.maxColumn)): + # Newline or wrap + if self.prevByte == '\n': + # Feed line (blank) + d += ((self.charHeight + + self.lineSpacing) * + self.dotFeedTime) + else: + # Text line + d += ((self.charHeight * + self.dotPrintTime) + + (self.lineSpacing * + self.dotFeedTime)) + self.column = 0 + # Treat wrap as newline + # on next pass + c = '\n' + else: + self.column += 1 + self.timeoutSet(d) + self.prevByte = c + + # The bulk of this method was moved into __init__, + # but this is left here for compatibility with older + # code that might get ported directly from Arduino. + def begin(self, heatTime=defaultHeatTime): + """Legacy method for setting up a new printer connection. + +The bulk of this method moved into `__init__`, but this method +has been left here for compatibility with older code that might +get ported directly from Arduino.""" + + self.writeBytes( + 27, # Esc + 55, # 7 (print settings) + 20, # Heat dots (20 = balance darkness w/no jams) + heatTime, # Lib default = 45 + 250) # Heat interval (500 uS = slower but darker) + + def reset(self): + """Resets printer settings to defaults.""" + + self.prevByte = '\n' # Treat as if prior line is blank + self.column = 0 + self.maxColumn = 32 + self.charHeight = 24 + self.lineSpacing = 8 + self.barcodeHeight = 50 + self.writeBytes(27, 64) + + # Reset text formatting parameters. + def setDefault(self): + """Resets text formatting parameters to defaults.""" + + self.online() + self.justify('L') + self.inverseOff() + self.doubleHeightOff() + self.setLineHeight(32) + self.boldOff() + self.underlineOff() + self.setBarcodeHeight(50) + self.setSize('s') + + def test(self): + """Self test method for checking printer/library performance.""" + + self.writeBytes(18, 84) + self.timeoutSet( + self.dotPrintTime * 24 * 26 + + self.dotFeedTime * (8 * 26 + 32)) + + + UPC_A = 0 + UPC_E = 1 + EAN13 = 2 + EAN8 = 3 + CODE39 = 4 + I25 = 5 + CODEBAR = 6 + CODE93 = 7 + CODE128 = 8 + CODE11 = 9 + MSI = 10 + + def printBarcode(self, text, type): + """Prints a barcode of the given type along with a text label. + +Barcode types and their numeric values are: +UPC_A | 0 +UPC_E | 1 +EAN13 | 2 +EAN8 | 3 +CODE39 | 4 +I25 | 5 +CODEBAR | 6 +CODE93 | 7 +CODE128 | 8 +CODE11 | 9 +MSI | 10.""" + + self.writeBytes( + 29, 72, 2, # Print label below barcode + 29, 119, 3, # Barcode width + 29, 107, type) # Barcode type + # Print string + self.timeoutWait() + self.timeoutSet((self.barcodeHeight + 40) * self.dotPrintTime) + super(Adafruit_Thermal, self).write(text) + self.prevByte = '\n' + self.feed(2) + + def setBarcodeHeight(self, val=50): + """Sets the height of a barcode print. + +The default is 50 dots.""" + + if val < 1: + val = 1 + self.barcodeHeight = val + self.writeBytes(29, 104, val) + + + # === Character commands === + + INVERSE_MASK = (1 << 1) + UPDOWN_MASK = (1 << 2) + BOLD_MASK = (1 << 3) + DOUBLE_HEIGHT_MASK = (1 << 4) + DOUBLE_WIDTH_MASK = (1 << 5) + STRIKE_MASK = (1 << 6) + + def setPrintMode(self, mask): + """Sets the printing mode. + +Possible modes are: +INVERSE_MASK +UPDOWN_MASK +BOLD_MASK +DOUBLE_HEIGHT_MASK +DOUBLE_WIDTH_MASK +STRIKE_MASK.""" + + self.printMode |= mask + self.writePrintMode() + if self.printMode & self.DOUBLE_HEIGHT_MASK: + self.charHeight = 48 + else: + self.charHeight = 24 + if self.printMode & self.DOUBLE_WIDTH_MASK: + self.maxColumn = 16 + else: + self.maxColumn = 32 - resumeTime = 0.0 - byteTime = 0.0 - dotPrintTime = 0.033 - dotFeedTime = 0.0025 - prevByte = '\n' - column = 0 - maxColumn = 32 - charHeight = 24 - lineSpacing = 8 - barcodeHeight = 50 - printMode = 0 - defaultHeatTime = 60 - - def __init__(self, *args, **kwargs): - # If no parameters given, use default port & baud rate. - # If only port is passed, use default baud rate. - # If both passed, use those values. - baudrate = 19200 - if len(args) == 0: - args = [ "/dev/ttyAMA0", baudrate ] - elif len(args) == 1: - args = [ args[0], baudrate ] - else: - baudrate = args[1] - - # Calculate time to issue one byte to the printer. - # 11 bits (not 8) to accommodate idle, start and stop bits. - # Idle time might be unnecessary, but erring on side of - # caution here. - self.byteTime = 11.0 / float(baudrate) - - Serial.__init__(self, *args, **kwargs) - - # Remainder of this method was previously in begin() - - # The printer can't start receiving data immediately upon - # power up -- it needs a moment to cold boot and initialize. - # Allow at least 1/2 sec of uptime before printer can - # receive data. - self.timeoutSet(0.5) - - self.wake() - self.reset() - - # Description of print settings from page 23 of the manual: - # ESC 7 n1 n2 n3 Setting Control Parameter Command - # Decimal: 27 55 n1 n2 n3 - # Set "max heating dots", "heating time", "heating interval" - # n1 = 0-255 Max heat dots, Unit (8dots), Default: 7 (64 dots) - # n2 = 3-255 Heating time, Unit (10us), Default: 80 (800us) - # n3 = 0-255 Heating interval, Unit (10us), Default: 2 (20us) - # The more max heating dots, the more peak current will cost - # when printing, the faster printing speed. The max heating - # dots is 8*(n1+1). The more heating time, the more density, - # but the slower printing speed. If heating time is too short, - # blank page may occur. The more heating interval, the more - # clear, but the slower printing speed. - - heatTime = kwargs.get('heattime', self.defaultHeatTime) - self.writeBytes( - 27, # Esc - 55, # 7 (print settings) - 20, # Heat dots (20 = balance darkness w/no jams) - heatTime, # Lib default = 45 - 250) # Heat interval (500 uS = slower but darker) - - # Description of print density from page 23 of the manual: - # DC2 # n Set printing density - # Decimal: 18 35 n - # D4..D0 of n is used to set the printing density. - # Density is 50% + 5% * n(D4-D0) printing density. - # D7..D5 of n is used to set the printing break time. - # Break time is n(D7-D5)*250us. - # (Unsure of the default value for either -- not documented) - - printDensity = 14 # 120% (can go higher, but text gets fuzzy) - printBreakTime = 4 # 500 uS - - self.writeBytes( - 18, # DC2 - 35, # Print density - (printBreakTime << 5) | printDensity) - - self.dotPrintTime = 0.03 - self.dotFeedTime = 0.0021 - - - # Because there's no flow control between the printer and computer, - # special care must be taken to avoid overrunning the printer's - # buffer. Serial output is throttled based on serial speed as well - # as an estimate of the device's print and feed rates (relatively - # slow, being bound to moving parts and physical reality). After - # an operation is issued to the printer (e.g. bitmap print), a - # timeout is set before which any other printer operations will be - # suspended. This is generally more efficient than using a delay - # in that it allows the calling code to continue with other duties - # (e.g. receiving or decoding an image) while the printer - # physically completes the task. - - # Sets estimated completion time for a just-issued task. - def timeoutSet(self, x): - self.resumeTime = time.time() + x - - # Waits (if necessary) for the prior task to complete. - def timeoutWait(self): - while (time.time() - self.resumeTime) < 0: pass - - - # Printer performance may vary based on the power supply voltage, - # thickness of paper, phase of the moon and other seemingly random - # variables. This method sets the times (in microseconds) for the - # paper to advance one vertical 'dot' when printing and feeding. - # For example, in the default initialized state, normal-sized text - # is 24 dots tall and the line spacing is 32 dots, so the time for - # one line to be issued is approximately 24 * print time + 8 * feed - # time. The default print and feed times are based on a random - # test unit, but as stated above your reality may be influenced by - # many factors. This lets you tweak the timing to avoid excessive - # delays and/or overrunning the printer buffer. - def setTimes(self, p, f): - # Units are in microseconds for - # compatibility with Arduino library - self.dotPrintTime = p / 1000000.0 - self.dotFeedTime = f / 1000000.0 - - - # 'Raw' byte-writing method - def writeBytes(self, *args): - self.timeoutWait() - self.timeoutSet(len(args) * self.byteTime) - for arg in args: - super(Adafruit_Thermal, self).write(chr(arg)) - - - # Override write() method to keep track of paper feed. - def write(self, *data): - for i in range(len(data)): - c = data[i] - if c != 0x13: - self.timeoutWait() - super(Adafruit_Thermal, self).write(c) - d = self.byteTime - if ((c == '\n') or - (self.column == self.maxColumn)): - # Newline or wrap - if self.prevByte == '\n': - # Feed line (blank) - d += ((self.charHeight + - self.lineSpacing) * - self.dotFeedTime) - else: - # Text line - d += ((self.charHeight * - self.dotPrintTime) + - (self.lineSpacing * - self.dotFeedTime)) - self.column = 0 - # Treat wrap as newline - # on next pass - c = '\n' - else: - self.column += 1 - self.timeoutSet(d) - self.prevByte = c - - - # The bulk of this method was moved into __init__, - # but this is left here for compatibility with older - # code that might get ported directly from Arduino. - def begin(self, heatTime=defaultHeatTime): - self.writeBytes( - 27, # Esc - 55, # 7 (print settings) - 20, # Heat dots (20 = balance darkness w/no jams) - heatTime, # Lib default = 45 - 250) # Heat interval (500 uS = slower but darker) - - - def reset(self): - self.prevByte = '\n' # Treat as if prior line is blank - self.column = 0 - self.maxColumn = 32 - self.charHeight = 24 - self.lineSpacing = 8 - self.barcodeHeight = 50 - self.writeBytes(27, 64) - - - # Reset text formatting parameters. - def setDefault(self): - self.online() - self.justify('L') - self.inverseOff() - self.doubleHeightOff() - self.setLineHeight(32) - self.boldOff() - self.underlineOff() - self.setBarcodeHeight(50) - self.setSize('s') - - - def test(self): - self.writeBytes(18, 84) - self.timeoutSet( - self.dotPrintTime * 24 * 26 + - self.dotFeedTime * (8 * 26 + 32)) - - - UPC_A = 0 - UPC_E = 1 - EAN13 = 2 - EAN8 = 3 - CODE39 = 4 - I25 = 5 - CODEBAR = 6 - CODE93 = 7 - CODE128 = 8 - CODE11 = 9 - MSI = 10 - - def printBarcode(self, text, type): - self.writeBytes( - 29, 72, 2, # Print label below barcode - 29, 119, 3, # Barcode width - 29, 107, type) # Barcode type - # Print string - self.timeoutWait() - self.timeoutSet((self.barcodeHeight + 40) * self.dotPrintTime) - super(Adafruit_Thermal, self).write(text) - self.prevByte = '\n' - self.feed(2) - - def setBarcodeHeight(self, val=50): - if val < 1: - val = 1 - self.barcodeHeight = val - self.writeBytes(29, 104, val) - - - # === Character commands === - - INVERSE_MASK = (1 << 1) - UPDOWN_MASK = (1 << 2) - BOLD_MASK = (1 << 3) - DOUBLE_HEIGHT_MASK = (1 << 4) - DOUBLE_WIDTH_MASK = (1 << 5) - STRIKE_MASK = (1 << 6) - - def setPrintMode(self, mask): - self.printMode |= mask - self.writePrintMode() - if self.printMode & self.DOUBLE_HEIGHT_MASK: - self.charHeight = 48 - else: - self.charHeight = 24 - if self.printMode & self.DOUBLE_WIDTH_MASK: - self.maxColumn = 16 - else: - self.maxColumn = 32 - - def unsetPrintMode(self, mask): - self.printMode &= ~mask - self.writePrintMode() - if self.printMode & self.DOUBLE_HEIGHT_MASK: - self.charHeight = 48 - else: - self.charHeight = 24 - if self.printMode & self.DOUBLE_WIDTH_MASK: - self.maxColumn = 16 - else: - self.maxColumn = 32 - - def writePrintMode(self): - self.writeBytes(27, 33, self.printMode) - - def normal(self): - self.printMode = 0 - self.writePrintMode() - - def inverseOn(self): - self.setPrintMode(self.INVERSE_MASK) - - def inverseOff(self): - self.unsetPrintMode(self.INVERSE_MASK) - - def upsideDownOn(self): - self.setPrintMode(self.UPDOWN_MASK) - - def upsideDownOff(self): - self.unsetPrintMode(self.UPDOWN_MASK) - - def doubleHeightOn(self): - self.setPrintMode(self.DOUBLE_HEIGHT_MASK) - - def doubleHeightOff(self): - self.unsetPrintMode(self.DOUBLE_HEIGHT_MASK) - - def doubleWidthOn(self): - self.setPrintMode(self.DOUBLE_WIDTH_MASK) - - def doubleWidthOff(self): - self.unsetPrintMode(self.DOUBLE_WIDTH_MASK) - - def strikeOn(self): - self.setPrintMode(self.STRIKE_MASK) - - def strikeOff(self): - self.unsetPrintMode(self.STRIKE_MASK) - - def boldOn(self): - self.setPrintMode(self.BOLD_MASK) - - def boldOff(self): - self.unsetPrintMode(self.BOLD_MASK) - - - def justify(self, value): - c = value.upper() - if c == 'C': - pos = 1 - elif c == 'R': - pos = 2 - else: - pos = 0 - self.writeBytes(0x1B, 0x61, pos) - - - # Feeds by the specified number of lines - def feed(self, x=1): - # The datasheet claims sending bytes 27, 100, will work, - # but it feeds much more than that. So it's done manually: - while x > 0: - self.write('\n') - x -= 1 - - - # Feeds by the specified number of individual pixel rows - def feedRows(self, rows): - self.writeBytes(27, 74, rows) - self.timeoutSet(rows * dotFeedTime) - - - def flush(self): - self.writeBytes(12) - - - def setSize(self, value): - c = value.upper() - if c == 'L': # Large: double width and height - size = 0x11 - self.charHeight = 48 - self.maxColumn = 16 - elif c == 'M': # Medium: double height - size = 0x01 - self.charHeight = 48 - self.maxColumn = 32 - else: # Small: standard width and height - size = 0x00 - self.charHeight = 24 - self.maxColumn = 32 - - self.writeBytes(29, 33, size, 10) - prevByte = '\n' # Setting the size adds a linefeed - - - # Underlines of different weights can be produced: - # 0 - no underline - # 1 - normal underline - # 2 - thick underline - def underlineOn(self, weight=1): - self.writeBytes(27, 45, weight) + def unsetPrintMode(self, mask): + """Disables only the given printing mode, while leaving any +other modes enabled.""" + self.printMode &= ~mask + self.writePrintMode() + if self.printMode & self.DOUBLE_HEIGHT_MASK: + self.charHeight = 48 + else: + self.charHeight = 24 + if self.printMode & self.DOUBLE_WIDTH_MASK: + self.maxColumn = 16 + else: + self.maxColumn = 32 - def underlineOff(self): - self.underlineOn(0) - - - def printBitmap(self, w, h, bitmap, LaaT=False): - rowBytes = (w + 7) / 8 # Round up to next byte boundary - if rowBytes >= 48: - rowBytesClipped = 48 # 384 pixels max width - else: - rowBytesClipped = rowBytes - - # if LaaT (line-at-a-time) is True, print bitmaps - # scanline-at-a-time (rather than in chunks). - # This tends to make for much cleaner printing - # (no feed gaps) on large images...but has the - # opposite effect on small images that would fit - # in a single 'chunk', so use carefully! - if LaaT: maxChunkHeight = 1 - else: maxChunkHeight = 255 - - i = 0 - for rowStart in range(0, h, maxChunkHeight): - chunkHeight = h - rowStart - if chunkHeight > maxChunkHeight: - chunkHeight = maxChunkHeight - - # Timeout wait happens here - self.writeBytes(18, 42, chunkHeight, rowBytesClipped) - - for y in range(chunkHeight): - for x in range(rowBytesClipped): - super(Adafruit_Thermal, self).write( - chr(bitmap[i])) - i += 1 - i += rowBytes - rowBytesClipped - self.timeoutSet(chunkHeight * self.dotPrintTime) - - self.prevByte = '\n' - - # Print Image. Requires Python Imaging Library. This is - # specific to the Python port and not present in the Arduino - # library. Image will be cropped to 384 pixels width if - # necessary, and converted to 1-bit w/diffusion dithering. - # For any other behavior (scale, B&W threshold, etc.), use - # the Imaging Library to perform such operations before - # passing the result to this function. - def printImage(self, image, LaaT=False): - import Image - - if image.mode != '1': - image = image.convert('1') - - width = image.size[0] - height = image.size[1] - if width > 384: - width = 384 - rowBytes = (width + 7) / 8 - bitmap = bytearray(rowBytes * height) - pixels = image.load() - - for y in range(height): - n = y * rowBytes - x = 0 - for b in range(rowBytes): - sum = 0 - bit = 128 - while bit > 0: - if x >= width: break - if pixels[x, y] == 0: - sum |= bit - x += 1 - bit >>= 1 - bitmap[n + b] = sum - - self.printBitmap(width, height, bitmap, LaaT) - - - # Take the printer offline. Print commands sent after this - # will be ignored until 'online' is called. - def offline(self): - self.writeBytes(27, 61, 0) - - - # Take the printer online. Subsequent print commands will be obeyed. - def online(self): - self.writeBytes(27, 61, 1) - - - # Put the printer into a low-energy state immediately. - def sleep(self): - self.sleepAfter(1) - - - # Put the printer into a low-energy state after - # the given number of seconds. - def sleepAfter(self, seconds): - self.writeBytes(27, 56, seconds) - - - def wake(self): - self.timeoutSet(0); - self.writeBytes(255) - for i in range(10): - self.writeBytes(27) - self.timeoutSet(0.1) - - - # Empty method, included for compatibility - # with existing code ported from Arduino. - def listen(self): - pass - - - # Check the status of the paper using the printers self reporting - # ability. Doesn't match the datasheet... - # Returns True for paper, False for no paper. - def hasPaper(self): - self.writeBytes(27, 118, 0) - # Bit 2 of response seems to be paper status - stat = ord(self.read(1)) & 0b00000100 - # If set, we have paper; if clear, no paper - return stat == 0 - - - def setLineHeight(self, val=32): - if val < 24: - val = 24 - self.lineSpacing = val - 24 - - # The printer doesn't take into account the current text - # height when setting line height, making this more akin - # to inter-line spacing. Default line spacing is 32 - # (char height of 24, line spacing of 8). - self.writeBytes(27, 51, val) - + def writePrintMode(self): + """Sends the configured printing mode to the printer.""" - # Copied from Arduino lib for parity; is marked 'not working' there - def tab(self): - self.writeBytes(9) - - - # Copied from Arduino lib for parity; is marked 'not working' there - def setCharSpacing(self, spacing): - self.writeBytes(27, 32, 0, 10) + self.writeBytes(27, 33, self.printMode) + def normal(self): + """Resets the print mode to the default 'nomal' mode.""" - # Overloading print() in Python pre-3.0 is dirty pool, - # but these are here to provide more direct compatibility - # with existing code written for the Arduino library. - def print(self, *args, **kwargs): - for arg in args: - self.write(str(arg)) + self.printMode = 0 + self.writePrintMode() - # For Arduino code compatibility again - def println(self, *args, **kwargs): - for arg in args: - self.write(str(arg)) - self.write('\n') + def inverseOn(self): + """Helper method for enabling inverted printing.""" + + self.setPrintMode(self.INVERSE_MASK) + + def inverseOff(self): + """Helper method for disabling inverted printing.""" + + self.unsetPrintMode(self.INVERSE_MASK) + + def upsideDownOn(self): + """Helper method for enabling upsidedown printing.""" + + self.setPrintMode(self.UPDOWN_MASK) + + def upsideDownOff(self): + """Helper method for disabling upsidedown printing.""" + + self.unsetPrintMode(self.UPDOWN_MASK) + + def doubleHeightOn(self): + """Helper method for enabling double-height printing.""" + + self.setPrintMode(self.DOUBLE_HEIGHT_MASK) + + def doubleHeightOff(self): + """Helper method for disabling double-height printing.""" + + self.unsetPrintMode(self.DOUBLE_HEIGHT_MASK) + + def doubleWidthOn(self): + """Helper method for enabling double-width printing.""" + + self.setPrintMode(self.DOUBLE_WIDTH_MASK) + + def doubleWidthOff(self): + """Helper method for disabling double-width printing.""" + + self.unsetPrintMode(self.DOUBLE_WIDTH_MASK) + + def strikeOn(self): + """Helper method for enabling strike-through printing.""" + + self.setPrintMode(self.STRIKE_MASK) + + def strikeOff(self): + """Helper method for disabling strike-through printing.""" + + self.unsetPrintMode(self.STRIKE_MASK) + + def boldOn(self): + """Helper method for enabling bold printing.""" + + self.setPrintMode(self.BOLD_MASK) + + def boldOff(self): + """Helper method for disabling bold printing.""" + + self.unsetPrintMode(self.BOLD_MASK) + + # Underlines of different weights can be produced: + # 0 - no underline + # 1 - normal underline + # 2 - thick underline + def underlineOn(self, weight=1): + """Helper method for enabling underline printing.""" + + self.writeBytes(27, 45, weight) + + def underlineOff(self): + """Helper method for disabling underline printing.""" + + self.underlineOn(0) + + def justify(self, value='L'): + """Sets the text justication method. + +Accepted values are: +C: center justified +R: right justified +L: left justified + +The default is left justified.""" + + c = value.upper() + if c == 'C': + pos = 1 + elif c == 'R': + pos = 2 + else: + pos = 0 + self.writeBytes(0x1B, 0x61, pos) + + # Feeds by the specified number of lines + def feed(self, x=1): + """Feeds the specified number of lines of paper.""" + + # The datasheet claims sending bytes 27, 100, will work, + # but it feeds much more than that. So it's done manually: + while x > 0: + self.write('\n') + x -= 1 + + # Feeds by the specified number of individual pixel rows + def feedRows(self, rows): + """Feeds the specified number of individual pixel rows.""" + + self.writeBytes(27, 74, rows) + self.timeoutSet(rows * dotFeedTime) + + def flush(self): + """Flushes the printer's buffer.""" + + self.writeBytes(12) + + + def setSize(self, value='S'): + """Sets the size of text printing. + +Accepted values are: +L: 'Large' (double height and double width) +M: 'Medium' (double height and normal width) +S: 'Small' (normal height and normal width) + +The default is 'S' for small/normal printing.""" + + c = value.upper() + if c == 'L': # Large: double width and height + size = 0x11 + self.charHeight = 48 + self.maxColumn = 16 + elif c == 'M': # Medium: double height + size = 0x01 + self.charHeight = 48 + self.maxColumn = 32 + else: # Small: standard width and height + size = 0x00 + self.charHeight = 24 + self.maxColumn = 32 + + self.writeBytes(29, 33, size, 10) + prevByte = '\n' # Setting the size adds a linefeed + + def printBitmap(self, w, h, bitmap, LaaT=False): + """Prints a bitmap image of width=w and height=h.""" + + rowBytes = (w + 7) / 8 # Round up to next byte boundary + if rowBytes >= 48: + rowBytesClipped = 48 # 384 pixels max width + else: + rowBytesClipped = rowBytes + + # if LaaT (line-at-a-time) is True, print bitmaps + # scanline-at-a-time (rather than in chunks). + # This tends to make for much cleaner printing + # (no feed gaps) on large images...but has the + # opposite effect on small images that would fit + # in a single 'chunk', so use carefully! + if LaaT: maxChunkHeight = 1 + else: maxChunkHeight = 255 + + i = 0 + for rowStart in range(0, h, maxChunkHeight): + chunkHeight = h - rowStart + if chunkHeight > maxChunkHeight: + chunkHeight = maxChunkHeight + + # Timeout wait happens here + self.writeBytes(18, 42, chunkHeight, rowBytesClipped) + + for y in range(chunkHeight): + for x in range(rowBytesClipped): + super(Adafruit_Thermal, self).write( + chr(bitmap[i])) + i += 1 + i += rowBytes - rowBytesClipped + self.timeoutSet(chunkHeight * self.dotPrintTime) + + self.prevByte = '\n' + + # Print Image. Requires Python Imaging Library. This is + # specific to the Python port and not present in the Arduino + # library. Image will be cropped to 384 pixels width if + # necessary, and converted to 1-bit w/diffusion dithering. + # For any other behavior (scale, B&W threshold, etc.), use + # the Imaging Library to perform such operations before + # passing the result to this function. + def printImage(self, image, LaaT=False): + """Prints an image using PIL. + +The image is first cropped to 384pixels wide (if needed) and is +then converted to 1-bit with diffusion dithering. + +Any other image manipulation should be done on the image prior +passing it this method.""" + + import Image + + if image.mode != '1': + image = image.convert('1') + + width = image.size[0] + height = image.size[1] + if width > 384: + width = 384 + rowBytes = (width + 7) / 8 + bitmap = bytearray(rowBytes * height) + pixels = image.load() + + for y in range(height): + n = y * rowBytes + x = 0 + for b in range(rowBytes): + sum = 0 + bit = 128 + while bit > 0: + if x >= width: break + if pixels[x, y] == 0: + sum |= bit + x += 1 + bit >>= 1 + bitmap[n + b] = sum + + self.printBitmap(width, height, bitmap, LaaT) + + + # Take the printer offline. Print commands sent after this + # will be ignored until 'online' is called. + def offline(self): + """Take the printer 'offline'. + +Print commands sent after this will be ignored until 'online()' +is called.""" + + self.writeBytes(27, 61, 0) + + + # Take the printer online. Subsequent print commands will be obeyed. + def online(self): + """Take the printer 'online'. + +This method does the opposite of 'offline' and all subsuquent +print commands will be obeyed.""" + + self.writeBytes(27, 61, 1) + + + # Put the printer into a low-energy state immediately. + def sleep(self): + """Immediately put the printer into a low-energy state.""" + + self.sleepAfter(1) + + + # Put the printer into a low-energy state after + # the given number of seconds. + def sleepAfter(self, seconds): + """Put the printer into a low-energy state after the given +number of seconds.""" + + self.writeBytes(27, 56, seconds) + + + def wake(self): + """Bring the printer out of a low-energy state back to +normal operation.""" + + self.timeoutSet(0); + self.writeBytes(255) + for i in range(10): + self.writeBytes(27) + self.timeoutSet(0.1) + + # Empty method, included for compatibility + # with existing code ported from Arduino. + def listen(self): + """Unimplemented method for compatibility with code ported from +Arduino.""" + + pass + + # Check the status of the paper using the printers self reporting + # ability. Doesn't match the datasheet... + # Returns True for paper, False for no paper. + def hasPaper(self): + """Check for the presence of paper using the printer's own +reporting ability. + +Returns True if paper is present, and False in all other cases.""" + + self.writeBytes(27, 118, 0) + # Bit 2 of response seems to be paper status + stat = ord(self.read(1)) & 0b00000100 + # If set, we have paper; if clear, no paper + return stat == 0 + + def setLineHeight(self, val=32): + """Sets the height (in dots) of each line.""" + + if val < 24: + val = 24 + self.lineSpacing = val - 24 + + # The printer doesn't take into account the current text + # height when setting line height, making this more akin + # to inter-line spacing. Default line spacing is 32 + # (char height of 24, line spacing of 8). + self.writeBytes(27, 51, val) + + # Copied from Arduino lib for parity; is marked 'not working' there + def tab(self): + """Send a 'tab' character to the printer. + +This is a direct port from the Arduino Thermal Printer library +in which this method is marked as 'not working'.""" + + self.writeBytes(9) + + # Copied from Arduino lib for parity; is marked 'not working' there + def setCharSpacing(self, spacing): + """Sets the character spacing/kerning. + +This is a direct port from the Arduino Thermal Printer library +in which this method is marked as 'not working'.""" + + self.writeBytes(27, 32, 0, 10) + + # Overloading print() in Python pre-3.0 is dirty pool, + # but these are here to provide more direct compatibility + # with existing code written for the Arduino library. + def print(self, *args, **kwargs): + """Overloads print to allow for easier porting of Arduino +code.""" + + for arg in args: + self.write(str(arg)) + + # For Arduino code compatibility again + def println(self, *args, **kwargs): + """Overloads println to allow for easier porting of Aruduino +code.""" + + for arg in args: + self.write(str(arg)) + self.write('\n') From 384efb0ac29bae8b661db0bd2cb22821c91fd4e1 Mon Sep 17 00:00:00 2001 From: zachwick Date: Mon, 14 Mar 2016 15:02:50 -0400 Subject: [PATCH 2/4] Makes Adafruit_Thermal a proper PyPi package Signed-off-by: zachwick More work moving to a PyPi package Signed-off-by: zachwick Removes the packaging TODO as it is done Signed-off-by: zachwick --- .gitignore | 3 + AUTHORS | 9 +++ .../Adafruit_Thermal.py | 4 +- Adafruit_Thermal/__init__.py | 28 ++++++++ COPYING | 22 +++++++ MANIFEST.in | 1 + README.md => README.rst | 0 calibrate.py => examples/calibrate.py | 0 forecast.py => examples/forecast.py | 0 {gfx => examples/gfx}/__init__.py | 0 {gfx => examples/gfx}/adalogo.py | 0 {gfx => examples/gfx}/adaqrcode.py | 0 {gfx => examples/gfx}/goodbye.png | Bin {gfx => examples/gfx}/hello.png | Bin {gfx => examples/gfx}/sudoku.png | Bin {gfx => examples/gfx}/timetemp.png | Bin main.py => examples/iot_printer.py | 0 printertest.py => examples/printertest.py | 0 sudoku-gfx.py => examples/sudoku-gfx.py | 0 sudoku-txt.py => examples/sudoku-txt.py | 0 timetemp.py => examples/timetemp.py | 0 twitter.py => examples/twitter.py | 0 setup.py | 61 ++++++++++++++++++ 23 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 AUTHORS rename Adafruit_Thermal.py => Adafruit_Thermal/Adafruit_Thermal.py (99%) create mode 100644 Adafruit_Thermal/__init__.py create mode 100644 COPYING create mode 100644 MANIFEST.in rename README.md => README.rst (100%) rename calibrate.py => examples/calibrate.py (100%) rename forecast.py => examples/forecast.py (100%) rename {gfx => examples/gfx}/__init__.py (100%) rename {gfx => examples/gfx}/adalogo.py (100%) rename {gfx => examples/gfx}/adaqrcode.py (100%) rename {gfx => examples/gfx}/goodbye.png (100%) rename {gfx => examples/gfx}/hello.png (100%) rename {gfx => examples/gfx}/sudoku.png (100%) rename {gfx => examples/gfx}/timetemp.png (100%) rename main.py => examples/iot_printer.py (100%) rename printertest.py => examples/printertest.py (100%) rename sudoku-gfx.py => examples/sudoku-gfx.py (100%) rename sudoku-txt.py => examples/sudoku-txt.py (100%) rename timetemp.py => examples/timetemp.py (100%) rename twitter.py => examples/twitter.py (100%) create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d7cf8e9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.pyc +__pycache__/ +*.egg-info diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..fd6368a --- /dev/null +++ b/AUTHORS @@ -0,0 +1,9 @@ +List compiled from copyright headers and git commit log +------------------------------------------------------- + +Limor Fried/Ladyada +Phillip Burgess +Brendan Smithyman +Ted M Lin +Masato Mukoda +zachwick diff --git a/Adafruit_Thermal.py b/Adafruit_Thermal/Adafruit_Thermal.py similarity index 99% rename from Adafruit_Thermal.py rename to Adafruit_Thermal/Adafruit_Thermal.py index e4e316a..6857fc2 100644 --- a/Adafruit_Thermal.py +++ b/Adafruit_Thermal/Adafruit_Thermal.py @@ -11,6 +11,7 @@ # # Written by Limor Fried/Ladyada for Adafruit Industries. # Python port by Phil Burgess for Adafruit Industries. +# See `AUTHORS` for a full list of contributors # MIT license, all text above must be included in any redistribution. #************************************************************************* @@ -28,7 +29,6 @@ # TO DO: # - Might use standard ConfigParser library to put thermal calibration # settings in a global configuration file (rather than in the library). -# - Make this use proper Python library installation procedure. # - Trap errors properly. Some stuff just falls through right now. # Python 2.X code using the library usu. needs to include the next line: @@ -583,7 +583,7 @@ def printImage(self, image, LaaT=False): Any other image manipulation should be done on the image prior passing it this method.""" - import Image + from PIL import Image if image.mode != '1': image = image.convert('1') diff --git a/Adafruit_Thermal/__init__.py b/Adafruit_Thermal/__init__.py new file mode 100644 index 0000000..0de09c1 --- /dev/null +++ b/Adafruit_Thermal/__init__.py @@ -0,0 +1,28 @@ +''' +Adadfruit_Thermal - a python library for interacting with TTL serial +thermal printers from Adafruit. + +Copyright 2013 2014, 2015, 2016 Adafruit Industries +See AUTHORS file for full list of contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +''' + +from .Adafruit_Thermal import * diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..0f45f9d --- /dev/null +++ b/COPYING @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2013, 2014, 2015, 2016 Adafruit Industries + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..c4bf456 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include README.rst \ No newline at end of file diff --git a/README.md b/README.rst similarity index 100% rename from README.md rename to README.rst diff --git a/calibrate.py b/examples/calibrate.py similarity index 100% rename from calibrate.py rename to examples/calibrate.py diff --git a/forecast.py b/examples/forecast.py similarity index 100% rename from forecast.py rename to examples/forecast.py diff --git a/gfx/__init__.py b/examples/gfx/__init__.py similarity index 100% rename from gfx/__init__.py rename to examples/gfx/__init__.py diff --git a/gfx/adalogo.py b/examples/gfx/adalogo.py similarity index 100% rename from gfx/adalogo.py rename to examples/gfx/adalogo.py diff --git a/gfx/adaqrcode.py b/examples/gfx/adaqrcode.py similarity index 100% rename from gfx/adaqrcode.py rename to examples/gfx/adaqrcode.py diff --git a/gfx/goodbye.png b/examples/gfx/goodbye.png similarity index 100% rename from gfx/goodbye.png rename to examples/gfx/goodbye.png diff --git a/gfx/hello.png b/examples/gfx/hello.png similarity index 100% rename from gfx/hello.png rename to examples/gfx/hello.png diff --git a/gfx/sudoku.png b/examples/gfx/sudoku.png similarity index 100% rename from gfx/sudoku.png rename to examples/gfx/sudoku.png diff --git a/gfx/timetemp.png b/examples/gfx/timetemp.png similarity index 100% rename from gfx/timetemp.png rename to examples/gfx/timetemp.png diff --git a/main.py b/examples/iot_printer.py similarity index 100% rename from main.py rename to examples/iot_printer.py diff --git a/printertest.py b/examples/printertest.py similarity index 100% rename from printertest.py rename to examples/printertest.py diff --git a/sudoku-gfx.py b/examples/sudoku-gfx.py similarity index 100% rename from sudoku-gfx.py rename to examples/sudoku-gfx.py diff --git a/sudoku-txt.py b/examples/sudoku-txt.py similarity index 100% rename from sudoku-txt.py rename to examples/sudoku-txt.py diff --git a/timetemp.py b/examples/timetemp.py similarity index 100% rename from timetemp.py rename to examples/timetemp.py diff --git a/twitter.py b/examples/twitter.py similarity index 100% rename from twitter.py rename to examples/twitter.py diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..4223cb8 --- /dev/null +++ b/setup.py @@ -0,0 +1,61 @@ +''' +Adadfruit_Thermal - a python library for interacting with TTL serial +thermal printers from Adafruit. + +Copyright 2013 2014, 2015, 2016 Adafruit Industries +See AUTHORS file for full list of contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +''' + +try: + from setuptools import setup +except ImportError: + from distutils.core import setup + +def readme(): + with open('README.rst') as f: + return f.read() + +setup(name='Adafruit_Thermal', + version='1.0.0', + description='Library for controlling Adafruit Thermal Printers', + url='https://github.com/bareo/Python-Thermal-Printer', + author='zach wick', + author_email='zwick@bareo.io', + license='MIT', + packages=['Adafruit_Thermal'], + zip_safe=False, + classifiers=[ + 'License :: OSI Approved :: MIT', + 'Intended Audience :: Developers', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 2.7', + ], + install_requires=[ + 'pyserial', + 'Pillow', + 'unidecode', + ], + keywords='adafruit thermal printer serial', + include_package_data=True, + test_suite='nose.collector', + tests_require=['nose', 'nose-cover3'], + ) From 9354f0a67c6d3f085ee366a8a911c39c56c0a22e Mon Sep 17 00:00:00 2001 From: zachwick Date: Mon, 14 Mar 2016 15:42:50 -0400 Subject: [PATCH 3/4] Makes a better README Signed-off-by: zachwick --- README.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.rst b/README.rst index 7af54a6..2a11591 100644 --- a/README.rst +++ b/README.rst @@ -1,2 +1,17 @@ Python-Thermal-Printer ====================== + +Installation:: + + pip install Adafruit_Thermal + +or:: + + python setup.py install + +Usage:: + + >>> import Adafruit_Thermal + >>> printer = Adafruit_Thermal('/dev/ttyAMA0', 19200, timeout=5) + >>> printer.println('Hello World!') + From 2a2e883e81db8a392f0d5ca4886059b7ce78b75c Mon Sep 17 00:00:00 2001 From: zachwick Date: Mon, 14 Mar 2016 15:55:21 -0400 Subject: [PATCH 4/4] Fixes a classifier Signed-off-by: zachwick --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4223cb8..3471145 100644 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ def readme(): packages=['Adafruit_Thermal'], zip_safe=False, classifiers=[ - 'License :: OSI Approved :: MIT', + 'License :: OSI Approved :: MIT License', 'Intended Audience :: Developers', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 2.7',