diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1e5e8c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/__pycache__/ + +# for Snyk +.dccache diff --git a/Adafruit_Thermal.py b/Adafruit_Thermal.py index f5e5162..551e251 100644 --- a/Adafruit_Thermal.py +++ b/Adafruit_Thermal.py @@ -32,99 +32,111 @@ # - 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 from serial import Serial import time +import sys +import math class Adafruit_Thermal(Serial): - 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 + resumeTime = 0.0 + byteTime = 0.0 + dotPrintTime = 0.0 + dotFeedTime = 0.0 + prevByte = '\n' + column = 0 + maxColumn = 32 + charHeight = 24 + lineSpacing = 8 + barcodeHeight = 50 + printMode = 0 + defaultHeatTime = 120 + firmwareVersion = 268 + writeToStdout = False 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. + # NEW BEHAVIOR: if no parameters given, output is written + # to stdout, to be piped through 'lp -o raw' (old behavior + # was to use default port & baud rate). baudrate = 19200 if len(args) == 0: - args = [ "/dev/ttyAMA0", baudrate ] - elif len(args) == 1: + self.writeToStdout = True + if len(args) == 1: + # If only port is passed, use default baud rate. args = [ args[0], baudrate ] - else: + elif len(args) == 2: + # If both passed, use those values. 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 - + # Firmware is assumed version 2.68. Can override this + # with the 'firmware=X' argument, where X is the major + # version number * 100 + the minor version number (e.g. + # pass "firmware=264" for version 2.64. + self.firmwareVersion = kwargs.get('firmware', 268) + + if self.writeToStdout is False: + # 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 p. 23 of manual: + # ESC 7 n1 n2 n3 Setting Control Parameter Command + # Decimal: 27 55 n1 n2 n3 + # 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) + 11, # Heat dots + heatTime, # Lib default + 40) # Heat interval + + # Description of print density from p. 23 of 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 default values -- not documented) + + printDensity = 10 # 100% + printBreakTime = 2 # 500 uS + + self.writeBytes( + 18, # DC2 + 35, # Print density + (printBreakTime << 5) | printDensity) + self.dotPrintTime = 0.03 + self.dotFeedTime = 0.0021 + else: + self.reset() # Inits some vars # Because there's no flow control between the printer and computer, # special care must be taken to avoid overrunning the printer's @@ -144,8 +156,8 @@ def timeoutSet(self, x): # Waits (if necessary) for the prior task to complete. def timeoutWait(self): - while (time.time() - self.resumeTime) < 0: pass - + if self.writeToStdout is False: + 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 @@ -164,19 +176,24 @@ def setTimes(self, p, f): 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)) - + if self.writeToStdout: + for arg in args: + sys.stdout.write(bytes([arg])) + else: + for arg in args: + self.timeoutWait() + self.timeoutSet(len(args) * self.byteTime) + super(Adafruit_Thermal, self).write(bytes([arg])) # Override write() method to keep track of paper feed. def write(self, *data): for i in range(len(data)): c = data[i] + if self.writeToStdout: + sys.stdout.write(c) + continue if c != 0x13: self.timeoutWait() super(Adafruit_Thermal, self).write(c) @@ -204,7 +221,6 @@ def write(self, *data): 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. @@ -212,20 +228,23 @@ 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) - + 11, # Heat dots + heatTime, + 40) # Heat interval def reset(self): + self.writeBytes(27, 64) # Esc @ = init command self.prevByte = '\n' # Treat as if prior line is blank self.column = 0 self.maxColumn = 32 self.charHeight = 24 - self.lineSpacing = 8 + self.lineSpacing = 6 self.barcodeHeight = 50 - self.writeBytes(27, 64) - + if self.firmwareVersion >= 264: + # Configure tab stops on recent printers + self.writeBytes(27, 68) # Set tab stops + self.writeBytes( 4, 8, 12, 16) # every 4 columns, + self.writeBytes(20, 24, 28, 0) # 0 is end-of-list. # Reset text formatting parameters. def setDefault(self): @@ -233,19 +252,28 @@ def setDefault(self): self.justify('L') self.inverseOff() self.doubleHeightOff() - self.setLineHeight(32) + self.setLineHeight(30) self.boldOff() self.underlineOff() self.setBarcodeHeight(50) self.setSize('s') - + self.setCharset() + self.setCodePage() def test(self): + self.write("Hello world!".encode('cp437', 'ignore')) + self.feed(2) + + def testPage(self): self.writeBytes(18, 84) self.timeoutSet( self.dotPrintTime * 24 * 26 + - self.dotFeedTime * (8 * 26 + 32)) + self.dotFeedTime * (6 * 26 + 30)) + def setBarcodeHeight(self, val=50): + if val < 1: val = 1 + self.barcodeHeight = val + self.writeBytes(29, 104, val) UPC_A = 0 UPC_E = 1 @@ -258,29 +286,79 @@ def test(self): CODE128 = 8 CODE11 = 9 MSI = 10 + ITF = 11 + CODABAR = 12 def printBarcode(self, text, type): + + newDict = { # UPC codes & values for firmwareVersion >= 264 + self.UPC_A : 65, + self.UPC_E : 66, + self.EAN13 : 67, + self.EAN8 : 68, + self.CODE39 : 69, + self.ITF : 70, + self.CODABAR : 71, + self.CODE93 : 72, + self.CODE128 : 73, + self.I25 : -1, # NOT IN NEW FIRMWARE + self.CODEBAR : -1, + self.CODE11 : -1, + self.MSI : -1 + } + oldDict = { # UPC codes & values for firmwareVersion < 264 + self.UPC_A : 0, + self.UPC_E : 1, + self.EAN13 : 2, + self.EAN8 : 3, + self.CODE39 : 4, + self.I25 : 5, + self.CODEBAR : 6, + self.CODE93 : 7, + self.CODE128 : 8, + self.CODE11 : 9, + self.MSI : 10, + self.ITF : -1, # NOT IN OLD FIRMWARE + self.CODABAR : -1 + } + + if self.firmwareVersion >= 264: + n = newDict[type] + else: + n = oldDict[type] + if n == -1: return + self.feed(1) # Recent firmware requires this? self.writeBytes( - 29, 72, 2, # Print label below barcode - 29, 119, 3, # Barcode width - 29, 107, type) # Barcode type - # Print string + 29, 72, 2, # Print label below barcode + 29, 119, 3, # Barcode width + 29, 107, n) # Barcode type self.timeoutWait() self.timeoutSet((self.barcodeHeight + 40) * self.dotPrintTime) - super(Adafruit_Thermal, self).write(text) + # Print string + if self.firmwareVersion >= 264: + # Recent firmware: write length byte + string sans NUL + n = len(text) + if n > 255: n = 255 + if self.writeToStdout: + sys.stdout.write((chr(n)).encode('cp437', 'ignore')) + for i in range(n): + sys.stdout.write(text[i].encode('utf-8', 'ignore')) + else: + super(Adafruit_Thermal, self).write((chr(n)).encode('utf-8', 'ignore')) + for i in range(n): + super(Adafruit_Thermal, + self).write(text[i].encode('utf-8', 'ignore')) + else: + # Older firmware: write string + NUL + if self.writeToStdout: + sys.stdout.write(text.encode('utf-8', 'ignore')) + else: + super(Adafruit_Thermal, self).write(text.encode('utf-8', 'ignore')) 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) + INVERSE_MASK = (1 << 1) # Not in 2.6.8 firmware (see inverseOn()) UPDOWN_MASK = (1 << 2) BOLD_MASK = (1 << 3) DOUBLE_HEIGHT_MASK = (1 << 4) @@ -319,10 +397,16 @@ def normal(self): self.writePrintMode() def inverseOn(self): - self.setPrintMode(self.INVERSE_MASK) + if self.firmwareVersion >= 268: + self.writeBytes(29, 66, 1) + else: + self.setPrintMode(self.INVERSE_MASK) def inverseOff(self): - self.unsetPrintMode(self.INVERSE_MASK) + if self.firmwareVersion >= 268: + self.writeBytes(29, 66, 0) + else: + self.unsetPrintMode(self.INVERSE_MASK) def upsideDownOn(self): self.setPrintMode(self.UPDOWN_MASK) @@ -354,7 +438,6 @@ def boldOn(self): def boldOff(self): self.unsetPrintMode(self.BOLD_MASK) - def justify(self, value): c = value.upper() if c == 'C': @@ -365,25 +448,30 @@ def justify(self, value): 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 + if self.firmwareVersion >= 264: + self.writeBytes(27, 100, x) + self.timeoutSet(self.dotFeedTime * self.charHeight) + self.prevByte = '\n' + self.column = 0 + else: + # datasheet claims sending bytes 27, 100, works, + # but it feeds much more than that. So, manually: + while x > 0: + self.write('\n'.encode('cp437', 'ignore')) + x -= 1 # Feeds by the specified number of individual pixel rows def feedRows(self, rows): self.writeBytes(27, 74, rows) self.timeoutSet(rows * dotFeedTime) - + self.prevByte = '\n' + self.column = 0 def flush(self): - self.writeBytes(12) - + self.writeBytes(12) # ASCII FF def setSize(self, value): c = value.upper() @@ -400,24 +488,22 @@ def setSize(self, value): self.charHeight = 24 self.maxColumn = 32 - self.writeBytes(29, 33, size, 10) + self.writeBytes(29, 33, size) 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): + if weight > 2: weight = 2 self.writeBytes(27, 45, weight) - def underlineOff(self): - self.underlineOn(0) - + self.writeBytes(27, 45, 0) def printBitmap(self, w, h, bitmap, LaaT=False): - rowBytes = (w + 7) / 8 # Round up to next byte boundary + rowBytes = math.floor((w + 7) / 8) # Round up to next byte boundary if rowBytes >= 48: rowBytesClipped = 48 # 384 pixels max width else: @@ -443,8 +529,11 @@ def printBitmap(self, w, h, bitmap, LaaT=False): for y in range(chunkHeight): for x in range(rowBytesClipped): - super(Adafruit_Thermal, self).write( - chr(bitmap[i])) + if self.writeToStdout: + sys.stdout.write(bytes([bitmap[i]])) + else: + super(Adafruit_Thermal, + self).write(bytes([bitmap[i]])) i += 1 i += rowBytes - rowBytesClipped self.timeoutSet(chunkHeight * self.dotPrintTime) @@ -458,9 +547,10 @@ def printBitmap(self, w, h, bitmap, LaaT=False): # 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 - + def printImage(self, image_file, LaaT=False): + from PIL import Image + # image = Image.open(image_file) + image = image_file if image.mode != '1': image = image.convert('1') @@ -468,7 +558,7 @@ def printImage(self, image, LaaT=False): height = image.size[1] if width > 384: width = 384 - rowBytes = (width + 7) / 8 + rowBytes = math.floor((width + 7) / 8) bitmap = bytearray(rowBytes * height) pixels = image.load() @@ -488,57 +578,58 @@ def printImage(self, image, LaaT=False): 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) - + self.sleepAfter(1) # Can't be 0, that means "don't sleep" # Put the printer into a low-energy state after # the given number of seconds. def sleepAfter(self, seconds): - self.writeBytes(27, 56, seconds) - + if self.firmwareVersion >= 264: + self.writeBytes(27, 56, seconds & 0xFF, seconds >> 8) + else: + self.writeBytes(27, 56, seconds) def wake(self): - self.timeoutSet(0); + self.timeoutSet(0) self.writeBytes(255) - for i in range(10): - self.writeBytes(27) - self.timeoutSet(0.1) - + if self.firmwareVersion >= 264: + time.sleep(0.05) # 50 ms + self.writeBytes(27, 118, 0) # Sleep off (important!) + else: + 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) + if self.firmwareVersion >= 264: + self.writeBytes(27, 118, 0) + else: + self.writeBytes(29, 114, 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 + if val < 24: val = 24 self.lineSpacing = val - 24 # The printer doesn't take into account the current text @@ -547,27 +638,98 @@ def setLineHeight(self, val=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 + CHARSET_USA = 0 + CHARSET_FRANCE = 1 + CHARSET_GERMANY = 2 + CHARSET_UK = 3 + CHARSET_DENMARK1 = 4 + CHARSET_SWEDEN = 5 + CHARSET_ITALY = 6 + CHARSET_SPAIN1 = 7 + CHARSET_JAPAN = 8 + CHARSET_NORWAY = 9 + CHARSET_DENMARK2 = 10 + CHARSET_SPAIN2 = 11 + CHARSET_LATINAMERICA = 12 + CHARSET_KOREA = 13 + CHARSET_SLOVENIA = 14 + CHARSET_CROATIA = 14 + CHARSET_CHINA = 15 + + # Alters some chars in ASCII 0x23-0x7E range; see datasheet + def setCharset(self, val=0): + if val > 15: val = 15 + self.writeBytes(27, 82, val) + + CODEPAGE_CP437 = 0 # USA, Standard Europe + CODEPAGE_KATAKANA = 1 + CODEPAGE_CP850 = 2 # Multilingual + CODEPAGE_CP860 = 3 # Portuguese + CODEPAGE_CP863 = 4 # Canadian-French + CODEPAGE_CP865 = 5 # Nordic + CODEPAGE_WCP1251 = 6 # Cyrillic + CODEPAGE_CP866 = 7 # Cyrillic #2 + CODEPAGE_MIK = 8 # Cyrillic/Bulgarian + CODEPAGE_CP755 = 9 # East Europe, Latvian 2 + CODEPAGE_IRAN = 10 + CODEPAGE_CP862 = 15 # Hebrew + CODEPAGE_WCP1252 = 16 # Latin 1 + CODEPAGE_WCP1253 = 17 # Greek + CODEPAGE_CP852 = 18 # Latin 2 + CODEPAGE_CP858 = 19 # Multilingual Latin 1 + Euro + CODEPAGE_IRAN2 = 20 + CODEPAGE_LATVIAN = 21 + CODEPAGE_CP864 = 22 # Arabic + CODEPAGE_ISO_8859_1 = 23 # West Europe + CODEPAGE_CP737 = 24 # Greek + CODEPAGE_WCP1257 = 25 # Baltic + CODEPAGE_THAI = 26 + CODEPAGE_CP720 = 27 # Arabic + CODEPAGE_CP855 = 28 + CODEPAGE_CP857 = 29 # Turkish + CODEPAGE_WCP1250 = 30 # Central Europe + CODEPAGE_CP775 = 31 + CODEPAGE_WCP1254 = 32 # Turkish + CODEPAGE_WCP1255 = 33 # Hebrew + CODEPAGE_WCP1256 = 34 # Arabic + CODEPAGE_WCP1258 = 35 # Vietnam + CODEPAGE_ISO_8859_2 = 36 # Latin 2 + CODEPAGE_ISO_8859_3 = 37 # Latin 3 + CODEPAGE_ISO_8859_4 = 38 # Baltic + CODEPAGE_ISO_8859_5 = 39 # Cyrillic + CODEPAGE_ISO_8859_6 = 40 # Arabic + CODEPAGE_ISO_8859_7 = 41 # Greek + CODEPAGE_ISO_8859_8 = 42 # Hebrew + CODEPAGE_ISO_8859_9 = 43 # Turkish + CODEPAGE_ISO_8859_15 = 44 # Latin 3 + CODEPAGE_THAI2 = 45 + CODEPAGE_CP856 = 46 + CODEPAGE_CP874 = 47 + + # Selects alt symbols for 'upper' ASCII values 0x80-0xFF + def setCodePage(self, val=0): + if val > 47: val = 47 + self.writeBytes(27, 116, val) + + # Copied from Arduino lib for parity; may not work on all printers def tab(self): self.writeBytes(9) + self.column = (self.column + 4) & 0xFC - - # Copied from Arduino lib for parity; is marked 'not working' there + # Copied from Arduino lib for parity; may not work on all printers def setCharSpacing(self, spacing): - self.writeBytes(27, 32, 0, 10) - + self.writeBytes(27, 32, spacing) # 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.write((str(arg)).encode('cp437', 'ignore')) # For Arduino code compatibility again def println(self, *args, **kwargs): for arg in args: - self.write(str(arg)) - self.write('\n') + self.write((str(arg)).encode('cp437', 'ignore')) + self.write('\n'.encode('cp437', 'ignore')) diff --git a/README.md b/README.md index 7af54a6..be18e24 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,52 @@ -Python-Thermal-Printer -====================== +# !!! NOTE !!! +# THIS REPOSITORY IS ARCHIVED AND IS NO LONGER SUPPORTED OR MAINTAINED + +# Python-Thermal-Printer Module + +Python3 port of the original Adafruit [Python-Thermal-Printer](https://github.com/adafruit/Python-Thermal-Printer) library. + +## Getting Started + +Install Raspbian Buster and Wire the printer according to [this](https://learn.adafruit.com/networked-thermal-printer-using-cups-and-raspberry-pi/connect-and-configure-printer). I powered the printer with the GPIO pins as well. + +Run a test to see if the printer is working by punching in these commands into the terminal. + +``` shell +stty -F /dev/serial0 19200 +echo -e "This is a test.\\n\\n\\n" > /dev/serial0 +``` + +### Installing + +Update the system and install prerequisites. + +``` shell +sudo apt-get update +sudo apt-get install git cups wiringpi build-essential libcups2-dev libcupsimage2-dev python3-serial python-pil python-unidecode +``` + +Install the printer driver. Don't worry about the warnings that g++ gives. + +``` shell +git clone https://github.com/adafruit/zj-58 +cd zj-58 +make +sudo ./install +``` + +Make the printer the default printer. This is useful if you are going to be doing other things with it. + +``` shell +sudo lpadmin -p ZJ-58 -E -v serial:/dev/serial0?baud=19200 -m zjiang/ZJ-58.ppd +sudo lpoptions -d ZJ-58 +``` + +Restart the system. Clone this repository and try to run *printertest.py*. + +``` shell +git clone https://github.com/galacticfan/Python-Thermal-Printer/ +cd Python-Thermal-Printer +python3 printertest.py +``` + +Let me know if you have any issues. diff --git a/calibrate.py b/calibrate.py index a06f7fa..e29585e 100755 --- a/calibrate.py +++ b/calibrate.py @@ -23,7 +23,7 @@ from __future__ import print_function from Adafruit_Thermal import * -printer = Adafruit_Thermal("/dev/ttyAMA0", 19200, timeout=5) +printer = Adafruit_Thermal("/dev/serial0", 19200, timeout=5) for i in range(0,256,15): printer.begin(i) diff --git a/datetime.py b/datetime.py new file mode 100755 index 0000000..bbfcc88 --- /dev/null +++ b/datetime.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 + +# Python3 script to print the current date and time, using +# the Adafruit_Thermal library, in ISO 8601 format. +# https://www.iso.org/iso-8601-date-and-time-format.html + +import time +from Adafruit_Thermal import * + +# Lines of margin (integer) +i_feed = 3 +# Seconds to pause (float) +f_pause = 1.0 + +# Define the printer port, speed, and timeout +printer = Adafruit_Thermal("/dev/ttyS0", 19200, timeout=5) + +# Build the date stamp in the format YYYY-MM-DD ex: "2021-12-25" +datestamp = time.strftime("%Y-%m-%d", time.gmtime()) +print ("Date in preferred format:", datestamp) + +# Build the time stamp in the format Thh:mm:ssZ ex: "T23:59:59Z" +timestamp = 'T' + time.strftime("%H:%M:%S", time.gmtime()) + 'Z' +print ("Time in preferred format:", timestamp) + +# Tell printer to sleep +printer.sleep() +# Sleep for the defined time in case we're called many times in a row +time.sleep(f_pause) +# Call wake() before printing again, even if reset +printer.wake() +# Restore printer to defaults +printer.setDefault() + +# Give a little room at the top +printer.feed(i_feed) +# Center justify +printer.justify('C') +# Large size +printer.setSize('L') +# Print the date +printer.println(datestamp) +# Print the time +printer.println(timestamp) +# Give a little room at the bottom +printer.feed(i_feed) diff --git a/forecast.py b/forecast.py index d985497..6353101 100755 --- a/forecast.py +++ b/forecast.py @@ -1,73 +1,72 @@ -#!/usr/bin/python - -# Weather forecast for Raspberry Pi w/Adafruit Mini Thermal Printer. -# Retrieves data from Yahoo! weather, prints current conditions and -# forecasts for next two days. See timetemp.py for a different -# weather example using nice bitmaps. -# Written by Adafruit Industries. MIT license. -# -# Required software includes Adafruit_Thermal and PySerial libraries. -# Other libraries used are part of stock Python install. -# -# Resources: -# http://www.adafruit.com/products/597 Mini Thermal Receipt Printer -# http://www.adafruit.com/products/600 Printer starter pack - from __future__ import print_function -import urllib, time from Adafruit_Thermal import * -from xml.dom.minidom import parseString +from datetime import date +import calendar +import urllib.request +import json -# WOEID indicates the geographic location for the forecast. It is -# not a ZIP code or other common indicator. Instead, it can be found -# by 'manually' visiting http://weather.yahoo.com, entering a location -# and requesting a forecast, then copy the number from the end of the -# current URL string and paste it here. -WOEID = '2459115' +printer = Adafruit_Thermal("/dev/serial0", 19200, timeout=5) +def getLink(dailyOrHourly): + latitude = "38.8894" #limit to four decimal digits + longitude = "-77.0352" #limit to four decimal digits + mainLink = "https://api.weather.gov/points/" + latitude + "," + longitude + response_main = urllib.request.urlopen(mainLink) + raw_data_main = response_main.read().decode() + data_main = json.loads(raw_data_main) + properties_main = data_main['properties'] + dailyLink = properties_main["forecast"] + hourlyLink = properties_main["forecastHourly"] + if dailyOrHourly == "daily": + return dailyLink + elif dailyOrHourly == "hourly": + return hourlyLink -# Dumps one forecast line to the printer -def forecast(idx): - tag = 'yweather:forecast' - day = dom.getElementsByTagName(tag)[idx].getAttribute('day') - lo = dom.getElementsByTagName(tag)[idx].getAttribute('low') - hi = dom.getElementsByTagName(tag)[idx].getAttribute('high') - cond = dom.getElementsByTagName(tag)[idx].getAttribute('text') - printer.print(day + ': low ' + lo ) - printer.print(deg) - printer.print(' high ' + hi) - printer.print(deg) - printer.println(' ' + cond) +url_daily = getLink("daily") +response_daily = urllib.request.urlopen(url_daily) +# status & reason +# print(response_daily.status, response_daily.reason) -printer = Adafruit_Thermal("/dev/ttyAMA0", 19200, timeout=5) -deg = chr(0xf8) # Degree symbol on thermal printer +raw_data_daily = response_daily.read().decode() +data_daily = json.loads(raw_data_daily) +forecast_periods_daily = data_daily['properties']['periods'] -# Fetch forecast data from Yahoo!, parse resulting XML -dom = parseString(urllib.urlopen( - 'http://weather.yahooapis.com/forecastrss?w=' + WOEID).read()) -# Print heading -printer.inverseOn() -printer.print('{:^32}'.format( - dom.getElementsByTagName('description')[0].firstChild.data)) -printer.inverseOff() +current_period_isDayTime = forecast_periods_daily[0]['isDaytime'] -# Print current conditions -printer.boldOn() -printer.print('{:^32}'.format('Current conditions:')) -printer.boldOff() -printer.print('{:^32}'.format( - dom.getElementsByTagName('pubDate')[0].firstChild.data)) -temp = dom.getElementsByTagName('yweather:condition')[0].getAttribute('temp') -cond = dom.getElementsByTagName('yweather:condition')[0].getAttribute('text') -printer.print(temp) -printer.print(deg) -printer.println(' ' + cond) -printer.boldOn() +if current_period_isDayTime: + day_index = 0 + night_index = 1 +else: + day_index = 1 + night_index = 0 -# Print forecast -printer.print('{:^32}'.format('Forecast:')) -printer.boldOff() -forecast(0) -forecast(1) +day_name = forecast_periods_daily[day_index]['name'] +hi_temp = forecast_periods_daily[day_index]['temperature'] +night_name = forecast_periods_daily[night_index]['name'] +lo_temp = forecast_periods_daily[night_index]['temperature'] +current_detailed_forecast = forecast_periods_daily[0]['detailedForecast'] + +url_hourly = getLink("hourly") +response_hourly = urllib.request.urlopen(url_hourly) +# status & reason +#print(response_hourly.status, response_hourly.reason) + +raw_data_hourly = response_hourly.read().decode() +data_hourly = json.loads(raw_data_hourly) +forecast_periods_hourly = data_hourly['properties']['periods'] +temperature = forecast_periods_hourly[0]['temperature'] +d = date.today() +week_day = calendar.day_name[date(d.year,d.month,d.day).weekday()] +month_text = calendar.month_name[d.month] +printer.underlineOn() +printer.print("It's " + week_day + ", " + month_text + " " + str(d.day) + "\n") +printer.underlineOff() +printer.boldOn() +printer.print(day_name + "'s Forecast \n") +printer.boldOff() +printer.print("Current temperature: " + str(temperature) + " F \n") +printer.print("High temperature: " + str(hi_temp) + " F \n") +printer.print("Low temperature: " + str(lo_temp) + " F \n") +printer.print(current_detailed_forecast + "\n") printer.feed(3) diff --git a/main.py b/main.py index 4af3456..2494677 100755 --- a/main.py +++ b/main.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # Main script for Adafruit Internet of Things Printer 2. Monitors button # for taps and holds, performs periodic actions (Twitter polling by default) @@ -16,8 +16,11 @@ from __future__ import print_function import RPi.GPIO as GPIO -import subprocess, time, Image, socket +import subprocess, time, socket +from PIL import Image from Adafruit_Thermal import * +from datetime import date +import calendar ledPin = 18 buttonPin = 23 @@ -26,13 +29,13 @@ nextInterval = 0.0 # Time of next recurring operation dailyFlag = False # Set after daily trigger occurs lastId = '1' # State information passed to/from interval script -printer = Adafruit_Thermal("/dev/ttyAMA0", 19200, timeout=5) +printer = Adafruit_Thermal("/dev/serial0", 19200, timeout=5) # Called when button is briefly tapped. Invokes time/temperature script. def tap(): GPIO.output(ledPin, GPIO.HIGH) # LED on while working - subprocess.call(["python", "timetemp.py"]) + subprocess.call(["python3", "forecast.py"]) GPIO.output(ledPin, GPIO.LOW) @@ -50,7 +53,7 @@ def hold(): # Invokes twitter script. def interval(): GPIO.output(ledPin, GPIO.HIGH) - p = subprocess.Popen(["python", "twitter.py", str(lastId)], + p = subprocess.Popen(["python3", "twitter.py", str(lastId)], stdout=subprocess.PIPE) GPIO.output(ledPin, GPIO.LOW) return p.communicate()[0] # Script pipes back lastId, returned to main @@ -60,8 +63,11 @@ def interval(): # Invokes weather forecast and sudoku-gfx scripts. def daily(): GPIO.output(ledPin, GPIO.HIGH) - subprocess.call(["python", "forecast.py"]) - subprocess.call(["python", "sudoku-gfx.py"]) + subprocess.call(["python3", "forecast.py"]) + d = date.today() + weekday = calendar.day_name[date(d.year,d.month,d.day).weekday()] + if weekday == "Saturday" or weekday == "Sunday": + subprocess.call(["python3", "sudoku-gfx.py"]) GPIO.output(ledPin, GPIO.LOW) @@ -158,9 +164,9 @@ def daily(): # Every 30 seconds, run Twitter scripts. 'lastId' is passed around # to preserve state between invocations. Probably simpler to do an # import thing. - if t > nextInterval: - nextInterval = t + 30.0 - result = interval() - if result is not None: - lastId = result.rstrip('\r\n') +# if t > nextInterval: +# nextInterval = t + 30.0 +# result = interval() +# if result is not None: +# lastId = result.rstrip('\r\n') diff --git a/printertest.py b/printertest.py index 8a79279..2664884 100755 --- a/printertest.py +++ b/printertest.py @@ -2,7 +2,7 @@ from Adafruit_Thermal import * -printer = Adafruit_Thermal("/dev/ttyAMA0", 19200, timeout=5) +printer = Adafruit_Thermal("/dev/serial0", 19200, timeout=5) # Test inverse on & off printer.inverseOn() @@ -61,7 +61,7 @@ import gfx.adaqrcode as adaqrcode printer.printBitmap(adaqrcode.width, adaqrcode.height, adaqrcode.data) printer.println("Adafruit!") -printer.feed(1) +printer.feed(2) printer.sleep() # Tell printer to sleep printer.wake() # Call wake() before printing again, even if reset diff --git a/sudoku-gfx.py b/sudoku-gfx.py index 91b5204..e955bbf 100755 --- a/sudoku-gfx.py +++ b/sudoku-gfx.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 # # Sudoku Generator and Solver in 250 lines of python # Copyright (c) 2006 David Bau. All rights reserved. @@ -23,9 +23,9 @@ from __future__ import print_function import sys, os, random, getopt, re from Adafruit_Thermal import * -import Image +from PIL import Image -printer = Adafruit_Thermal("/dev/ttyAMA0", 19200, timeout=5) +printer = Adafruit_Thermal("/dev/serial0", 19200, timeout=5) bg = Image.new("1", [384, 426], "white") # Working 'background' image img = Image.open('gfx/sudoku.png') # Source bitmaps xcoord = [ 15, 55, 95, 139, 179, 219, 263, 303, 343 ] @@ -55,14 +55,14 @@ def main(): def makepuzzle(board): puzzle = []; deduced = [None] * 81 - order = random.sample(xrange(81), 81) + order = random.sample(range(81), 81) for pos in order: if deduced[pos] is None: puzzle.append((pos, board[pos])) deduced[pos] = board[pos] deduce(deduced) random.shuffle(puzzle) - for i in xrange(len(puzzle) - 1, -1, -1): + for i in range(len(puzzle) - 1, -1, -1): e = puzzle[i]; del puzzle[i] rating = checkpuzzle(boardforentries(puzzle), board) if rating == -1: puzzle.append(e) @@ -70,7 +70,7 @@ def makepuzzle(board): def ratepuzzle(puzzle, samples): total = 0 - for i in xrange(samples): + for i in range(samples): state, answer = solveboard(puzzle) if answer is None: return -1 total += len(state) @@ -113,7 +113,7 @@ def deduce(board): stuck, guess, count = True, None, 0 # fill in any spots determined by direct conflicts allowed, needed = figurebits(board) - for pos in xrange(81): + for pos in range(81): if None == board[pos]: numbers = listbits(allowed[pos]) if len(numbers) == 0: return [] @@ -122,13 +122,13 @@ def deduce(board): guess, count = pickbetter(guess, count, [(pos, n) for n in numbers]) if not stuck: allowed, needed = figurebits(board) # fill in any spots determined by elimination of other locations - for axis in xrange(3): - for x in xrange(9): + for axis in range(3): + for x in range(9): numbers = listbits(needed[axis * 9 + x]) for n in numbers: bit = 1 << n spots = [] - for y in xrange(9): + for y in range(9): pos = posfor(x, y, axis) if allowed[pos] & bit: spots.append(pos) if len(spots) == 0: return [] @@ -141,11 +141,11 @@ def deduce(board): def figurebits(board): allowed, needed = [e is None and 511 or 0 for e in board], [] - for axis in xrange(3): - for x in xrange(9): + for axis in range(3): + for x in range(9): bits = axismissing(board, x, axis) needed.append(bits) - for y in xrange(9): + for y in range(9): allowed[posfor(x, y, axis)] &= bits return allowed, needed @@ -161,17 +161,17 @@ def axisfor(pos, axis): def axismissing(board, x, axis): bits = 0 - for y in xrange(9): + for y in range(9): e = board[posfor(x, y, axis)] if e is not None: bits |= 1 << e return 511 ^ bits def listbits(bits): - return [y for y in xrange(9) if 0 != bits & 1 << y] + return [y for y in range(9) if 0 != bits & 1 << y] def allowed(board, pos): bits = 511 - for axis in xrange(3): + for axis in range(3): x = axisfor(pos, axis) bits &= axismissing(board, x, axis) return bits @@ -183,7 +183,7 @@ def pickbetter(b, c, t): else: return (b, c + 1) def entriesforboard(board): - return [(pos, board[pos]) for pos in xrange(81) if board[pos] is not None] + return [(pos, board[pos]) for pos in range(81) if board[pos] is not None] def boardforentries(entries): board = [None] * 81 @@ -191,14 +191,14 @@ def boardforentries(entries): return board def boardmatches(b1, b2): - for i in xrange(81): + for i in range(81): if b1[i] != b2[i]: return False return True def printboard(board): bg.paste(img, (0, 0)) # Numbers are cropped off right side - for row in xrange(9): - for col in xrange(9): + for row in range(9): + for col in range(9): n = board[posfor(row, col)] if n is not None: bg.paste(numbers[n], (xcoord[col], ycoord[row])) diff --git a/sudoku-txt.py b/sudoku-txt.py index 5e9a4db..5ae735c 100755 --- a/sudoku-txt.py +++ b/sudoku-txt.py @@ -22,7 +22,7 @@ import sys, os, random, getopt, re from Adafruit_Thermal import * -printer = Adafruit_Thermal("/dev/ttyAMA0", 19200, timeout=5) +printer = Adafruit_Thermal("/dev/serial0", 19200, timeout=5) def main(): printer.setLineHeight(24) # So graphical chars fit together diff --git a/timetemp.py b/timetemp.py index 2835782..2764fea 100755 --- a/timetemp.py +++ b/timetemp.py @@ -1,7 +1,7 @@ #!/usr/bin/python # Current time and temperature display for Raspberry Pi w/Adafruit Mini -# Thermal Printer. Retrieves data from Yahoo! weather, prints current +# Thermal Printer. Retrieves data from DarkSky.net's API, prints current # conditions and time using large, friendly graphics. # See forecast.py for a different weather example that's all text-based. # Written by Adafruit Industries. MIT license. @@ -15,31 +15,31 @@ from __future__ import print_function from Adafruit_Thermal import * -from xml.dom.minidom import parseString -import Image, ImageDraw, time, urllib +import time, urllib.request, json +from PIL import Image, ImageDraw -# WOEID indicates the geographic location for the forecast. It is -# not a ZIP code or other common indicator. Instead, it can be found -# by 'manually' visiting http://weather.yahoo.com, entering a location -# and requesting a forecast, then copy the number from the end of the -# current URL string and paste it here. -WOEID = '2459115' +API_KEY = "YOUR_OPEN_WEATHER_API_KEY" -# Fetch weather data from Yahoo!, parse resulting XML -dom = parseString(urllib.urlopen( - 'http://weather.yahooapis.com/forecastrss?w=' + WOEID).read()) +cityName = "YOUR_CITY_NAME" +# Fetch weather data from DarkSky, parse resulting JSON +url = f"http://api.openweathermap.org/data/2.5/weather?q={cityName}&appid={API_KEY}" +response = urllib.request.urlopen(url) +data = json.loads(response.read()) +print(data) # Extract values relating to current temperature, humidity, wind -temperature = int(dom.getElementsByTagName( - 'yweather:condition')[0].getAttribute('temp')) -humidity = int(dom.getElementsByTagName( - 'yweather:atmosphere')[0].getAttribute('humidity')) -windSpeed = int(dom.getElementsByTagName( - 'yweather:wind')[0].getAttribute('speed')) -windDir = int(dom.getElementsByTagName( - 'yweather:wind')[0].getAttribute('direction')) -windUnits = dom.getElementsByTagName( - 'yweather:units')[0].getAttribute('speed') + +temperature = (int(data['main']['temp']) - 273.15) * 9/5 + 32 +humidity = int(data['main']['humidity'] * 100); +windSpeed = int(data['wind']['speed']) +windDir = data['wind']['deg'] +windUnits = "mph" + +# print(temperature) +# print(humidity) +# print(windSpeed) +# print(windDir) +# print(windUnits) # Although the Python Imaging Library does have nice font support, # I opted here to use a raster bitmap for all of the glyphs instead. @@ -62,11 +62,11 @@ # Generate a list of sub-image glyphs cropped from the symbols image def croplist(widths, x, y, height): - list = [] - for i in range(len(widths)): - list.append(symbols.crop( - [x, y+i*height, x+widths[i], y+(i+1)*height])) - return list + list = [] + for i in range(len(widths)): + list.append(symbols.crop( + [x, y+i*height, x+widths[i], y+(i+1)*height])) + return list # Crop glyph lists (digits, days of week, etc.) TimeDigit = croplist(TimeDigitWidth, 0, 0, 44) @@ -91,20 +91,20 @@ def croplist(widths, x, y, height): # Paste a series of glyphs (mostly numbers) from string to img def drawNums(str, x, y, list): - for i in range(len(str)): - d = ord(str[i]) - ord('0') - img.paste(list[d], (x, y)) - x += list[d].size[0] + 1 - return x + for i in range(len(str)): + d = ord(str[i]) - ord('0') + img.paste(list[d], (x, y)) + x += list[d].size[0] + 1 + return x # Determine total width of a series of glyphs in string def numWidth(str, list): - w = 0 # Cumulative width - for i in range(len(str)): - d = ord(str[i]) - ord('0') - if i > 0: w += 1 # Space between digits - w += list[d].size[0] # Digit width - return w + w = 0 # Cumulative width + for i in range(len(str)): + d = ord(str[i]) - ord('0') + if i > 0: w += 1 # Space between digits + w += list[d].size[0] # Digit width + return w # Render current time (always 24 hour XX:XX format) t = time.localtime() @@ -134,12 +134,13 @@ def numWidth(str, list): s2 = str(windSpeed) winDirNum = 0 # Wind direction glyph number if windSpeed > 0: - for winDirNum in range(len(DirAngle) - 1): - if windDir < DirAngle[winDirNum]: break + for winDirNum in range(len(DirAngle) - 1): + if windDir < DirAngle[winDirNum]: break +winDirNum+=1 w = Humidity.size[0] + 5 + numWidth(s, HumiDigit) w2 = Wind.size[0] + 5 + numWidth(s2, HumiDigit) if windSpeed > 0: - w2 += 3 + Dir[winDirNum].size[0] + w2 += 3 + Dir[winDirNum].size[0] if windUnits == 'kph': w2 += 3 + Kph.size[0] else: w2 += 3 + Mph.size[0] if w2 > w: w = w2 @@ -154,14 +155,15 @@ def numWidth(str, list): y += 23 # And advance to next line img.paste(Wind, (x, y)) x += Wind.size[0] + 5 + if windSpeed > 0: - img.paste(Dir[winDirNum], (x, y)) - x += Dir[winDirNum].size[0] + 3 + img.paste(Dir[winDirNum], (x, y)) + x += Dir[winDirNum].size[0] + 3 x = drawNums(s2, x, y, HumiDigit) + 3 if windUnits == 'kph': img.paste(Kph, (x, y)) else: img.paste(Mph, (x, y)) # Open connection to printer and print image -printer = Adafruit_Thermal("/dev/ttyAMA0", 19200, timeout=5) +printer = Adafruit_Thermal("/dev/serial0", 19200, timeout=5) printer.printImage(img, True) printer.feed(3) diff --git a/twitter.py b/twitter.py index 4b579d7..1d980ba 100755 --- a/twitter.py +++ b/twitter.py @@ -39,7 +39,7 @@ consumer_secret = 'PUT_YOUR_CONSUMER_SECRET_HERE' # queryString can be any valid Twitter API search string, including -# boolean operators. See http://dev.twitter.com/docs/using-search +# boolean operators. See https://developer.twitter.com/en/docs/tweets/search/api-reference/get-search-tweets # for options and syntax. Funny characters do NOT need to be URL # encoded here -- urllib takes care of that. queryString = 'from:Adafruit' @@ -47,7 +47,7 @@ # Other globals. You probably won't need to change these. ----------------- -printer = Adafruit_Thermal("/dev/ttyAMA0", 19200, timeout=5) +printer = Adafruit_Thermal("/dev/serial0", 19200, timeout=5) host = 'api.twitter.com' authUrl = '/oauth2/token' searchUrl = '/1.1/search/tweets.json?' @@ -62,7 +62,7 @@ def issueRequestAndDecodeResponse(method, url, body, headers): connection.request(method, url, body, headers) response = connection.getresponse() if response.status != 200: - # This is OK for command-line testing, otherwise + # This is OK for command-line testing, otherwise # keep it commented out when using main.py # print('HTTP error: %d' % response.status) exit(-1) @@ -101,6 +101,8 @@ def issueRequestAndDecodeResponse(method, url, body, headers): # Display results. --------------------------------------------------------- +maxId = data['search_metadata']['max_id_str'] + for tweet in data['statuses']: printer.inverseOn() @@ -111,6 +113,10 @@ def issueRequestAndDecodeResponse(method, url, body, headers): printer.print('{:<32}'.format(tweet['created_at'])) printer.underlineOff() + # max_id_str is not always present, so check tweet IDs as fallback + id = tweet['id_str'] + if(id > maxId): maxId = id # String compare is OK for this + # Remove HTML escape sequences # and remap Unicode values to nearest ASCII equivalents printer.print(unidecode( @@ -118,4 +124,4 @@ def issueRequestAndDecodeResponse(method, url, body, headers): printer.feed(3) -print(data['search_metadata']['max_id_str']) # Piped back to calling process +print(maxId) # Piped back to calling process