├── .gitignore ├── Adafruit_Thermal.py ├── README.md ├── calibrate.py ├── datetime.py ├── forecast.py ├── gfx ├── __init__.py ├── adalogo.py ├── adaqrcode.py ├── goodbye.png ├── hello.png ├── sudoku.png └── timetemp.png ├── main.py ├── printertest.py ├── sudoku-gfx.py ├── sudoku-txt.py ├── timetemp.py └── twitter.py /.gitignore: -------------------------------------------------------------------------------- 1 | /__pycache__/ 2 | 3 | # for Snyk 4 | .dccache 5 | -------------------------------------------------------------------------------- /Adafruit_Thermal.py: -------------------------------------------------------------------------------- 1 | #************************************************************************* 2 | # This is a Python library for the Adafruit Thermal Printer. 3 | # Pick one up at --> http://www.adafruit.com/products/597 4 | # These printers use TTL serial to communicate, 2 pins are required. 5 | # IMPORTANT: On 3.3V systems (e.g. Raspberry Pi), use a 10K resistor on 6 | # the RX pin (TX on the printer, green wire), or simply leave unconnected. 7 | # 8 | # Adafruit invests time and resources providing this open source code. 9 | # Please support Adafruit and open-source hardware by purchasing products 10 | # from Adafruit! 11 | # 12 | # Written by Limor Fried/Ladyada for Adafruit Industries. 13 | # Python port by Phil Burgess for Adafruit Industries. 14 | # MIT license, all text above must be included in any redistribution. 15 | #************************************************************************* 16 | 17 | # This is pretty much a 1:1 direct Python port of the Adafruit_Thermal 18 | # library for Arduino. All methods use the same naming conventions as the 19 | # Arduino library, with only slight changes in parameter behavior where 20 | # needed. This should simplify porting existing Adafruit_Thermal-based 21 | # printer projects to Raspberry Pi, BeagleBone, etc. See printertest.py 22 | # for an example. 23 | # 24 | # One significant change is the addition of the printImage() function, 25 | # which ties this to the Python Imaging Library and opens the door to a 26 | # lot of cool graphical stuff! 27 | # 28 | # TO DO: 29 | # - Might use standard ConfigParser library to put thermal calibration 30 | # settings in a global configuration file (rather than in the library). 31 | # - Make this use proper Python library installation procedure. 32 | # - Trap errors properly. Some stuff just falls through right now. 33 | # - Add docstrings throughout! 34 | 35 | from serial import Serial 36 | import time 37 | import sys 38 | import math 39 | 40 | class Adafruit_Thermal(Serial): 41 | 42 | resumeTime = 0.0 43 | byteTime = 0.0 44 | dotPrintTime = 0.0 45 | dotFeedTime = 0.0 46 | prevByte = '\n' 47 | column = 0 48 | maxColumn = 32 49 | charHeight = 24 50 | lineSpacing = 8 51 | barcodeHeight = 50 52 | printMode = 0 53 | defaultHeatTime = 120 54 | firmwareVersion = 268 55 | writeToStdout = False 56 | 57 | def __init__(self, *args, **kwargs): 58 | # NEW BEHAVIOR: if no parameters given, output is written 59 | # to stdout, to be piped through 'lp -o raw' (old behavior 60 | # was to use default port & baud rate). 61 | baudrate = 19200 62 | if len(args) == 0: 63 | self.writeToStdout = True 64 | if len(args) == 1: 65 | # If only port is passed, use default baud rate. 66 | args = [ args[0], baudrate ] 67 | elif len(args) == 2: 68 | # If both passed, use those values. 69 | baudrate = args[1] 70 | 71 | # Firmware is assumed version 2.68. Can override this 72 | # with the 'firmware=X' argument, where X is the major 73 | # version number * 100 + the minor version number (e.g. 74 | # pass "firmware=264" for version 2.64. 75 | self.firmwareVersion = kwargs.get('firmware', 268) 76 | 77 | if self.writeToStdout is False: 78 | # Calculate time to issue one byte to the printer. 79 | # 11 bits (not 8) to accommodate idle, start and 80 | # stop bits. Idle time might be unnecessary, but 81 | # erring on side of caution here. 82 | self.byteTime = 11.0 / float(baudrate) 83 | 84 | Serial.__init__(self, *args, **kwargs) 85 | 86 | # Remainder of this method was previously in begin() 87 | 88 | # The printer can't start receiving data immediately 89 | # upon power up -- it needs a moment to cold boot 90 | # and initialize. Allow at least 1/2 sec of uptime 91 | # before printer can receive data. 92 | self.timeoutSet(0.5) 93 | 94 | self.wake() 95 | self.reset() 96 | 97 | # Description of print settings from p. 23 of manual: 98 | # ESC 7 n1 n2 n3 Setting Control Parameter Command 99 | # Decimal: 27 55 n1 n2 n3 100 | # max heating dots, heating time, heating interval 101 | # n1 = 0-255 Max heat dots, Unit (8dots), Default: 7 (64 dots) 102 | # n2 = 3-255 Heating time, Unit (10us), Default: 80 (800us) 103 | # n3 = 0-255 Heating interval, Unit (10us), Default: 2 (20us) 104 | # The more max heating dots, the more peak current 105 | # will cost when printing, the faster printing speed. 106 | # The max heating dots is 8*(n1+1). The more heating 107 | # time, the more density, but the slower printing 108 | # speed. If heating time is too short, blank page 109 | # may occur. The more heating interval, the more 110 | # clear, but the slower printing speed. 111 | 112 | heatTime = kwargs.get('heattime', self.defaultHeatTime) 113 | self.writeBytes( 114 | 27, # Esc 115 | 55, # 7 (print settings) 116 | 11, # Heat dots 117 | heatTime, # Lib default 118 | 40) # Heat interval 119 | 120 | # Description of print density from p. 23 of manual: 121 | # DC2 # n Set printing density 122 | # Decimal: 18 35 n 123 | # D4..D0 of n is used to set the printing density. 124 | # Density is 50% + 5% * n(D4-D0) printing density. 125 | # D7..D5 of n is used to set the printing break time. 126 | # Break time is n(D7-D5)*250us. 127 | # (Unsure of default values -- not documented) 128 | 129 | printDensity = 10 # 100% 130 | printBreakTime = 2 # 500 uS 131 | 132 | self.writeBytes( 133 | 18, # DC2 134 | 35, # Print density 135 | (printBreakTime << 5) | printDensity) 136 | self.dotPrintTime = 0.03 137 | self.dotFeedTime = 0.0021 138 | else: 139 | self.reset() # Inits some vars 140 | 141 | # Because there's no flow control between the printer and computer, 142 | # special care must be taken to avoid overrunning the printer's 143 | # buffer. Serial output is throttled based on serial speed as well 144 | # as an estimate of the device's print and feed rates (relatively 145 | # slow, being bound to moving parts and physical reality). After 146 | # an operation is issued to the printer (e.g. bitmap print), a 147 | # timeout is set before which any other printer operations will be 148 | # suspended. This is generally more efficient than using a delay 149 | # in that it allows the calling code to continue with other duties 150 | # (e.g. receiving or decoding an image) while the printer 151 | # physically completes the task. 152 | 153 | # Sets estimated completion time for a just-issued task. 154 | def timeoutSet(self, x): 155 | self.resumeTime = time.time() + x 156 | 157 | # Waits (if necessary) for the prior task to complete. 158 | def timeoutWait(self): 159 | if self.writeToStdout is False: 160 | while (time.time() - self.resumeTime) < 0: pass 161 | 162 | # Printer performance may vary based on the power supply voltage, 163 | # thickness of paper, phase of the moon and other seemingly random 164 | # variables. This method sets the times (in microseconds) for the 165 | # paper to advance one vertical 'dot' when printing and feeding. 166 | # For example, in the default initialized state, normal-sized text 167 | # is 24 dots tall and the line spacing is 32 dots, so the time for 168 | # one line to be issued is approximately 24 * print time + 8 * feed 169 | # time. The default print and feed times are based on a random 170 | # test unit, but as stated above your reality may be influenced by 171 | # many factors. This lets you tweak the timing to avoid excessive 172 | # delays and/or overrunning the printer buffer. 173 | def setTimes(self, p, f): 174 | # Units are in microseconds for 175 | # compatibility with Arduino library 176 | self.dotPrintTime = p / 1000000.0 177 | self.dotFeedTime = f / 1000000.0 178 | 179 | # 'Raw' byte-writing method 180 | def writeBytes(self, *args): 181 | if self.writeToStdout: 182 | for arg in args: 183 | sys.stdout.write(bytes([arg])) 184 | else: 185 | for arg in args: 186 | self.timeoutWait() 187 | self.timeoutSet(len(args) * self.byteTime) 188 | super(Adafruit_Thermal, self).write(bytes([arg])) 189 | 190 | # Override write() method to keep track of paper feed. 191 | def write(self, *data): 192 | for i in range(len(data)): 193 | c = data[i] 194 | if self.writeToStdout: 195 | sys.stdout.write(c) 196 | continue 197 | if c != 0x13: 198 | self.timeoutWait() 199 | super(Adafruit_Thermal, self).write(c) 200 | d = self.byteTime 201 | if ((c == '\n') or 202 | (self.column == self.maxColumn)): 203 | # Newline or wrap 204 | if self.prevByte == '\n': 205 | # Feed line (blank) 206 | d += ((self.charHeight + 207 | self.lineSpacing) * 208 | self.dotFeedTime) 209 | else: 210 | # Text line 211 | d += ((self.charHeight * 212 | self.dotPrintTime) + 213 | (self.lineSpacing * 214 | self.dotFeedTime)) 215 | self.column = 0 216 | # Treat wrap as newline 217 | # on next pass 218 | c = '\n' 219 | else: 220 | self.column += 1 221 | self.timeoutSet(d) 222 | self.prevByte = c 223 | 224 | # The bulk of this method was moved into __init__, 225 | # but this is left here for compatibility with older 226 | # code that might get ported directly from Arduino. 227 | def begin(self, heatTime=defaultHeatTime): 228 | self.writeBytes( 229 | 27, # Esc 230 | 55, # 7 (print settings) 231 | 11, # Heat dots 232 | heatTime, 233 | 40) # Heat interval 234 | 235 | def reset(self): 236 | self.writeBytes(27, 64) # Esc @ = init command 237 | self.prevByte = '\n' # Treat as if prior line is blank 238 | self.column = 0 239 | self.maxColumn = 32 240 | self.charHeight = 24 241 | self.lineSpacing = 6 242 | self.barcodeHeight = 50 243 | if self.firmwareVersion >= 264: 244 | # Configure tab stops on recent printers 245 | self.writeBytes(27, 68) # Set tab stops 246 | self.writeBytes( 4, 8, 12, 16) # every 4 columns, 247 | self.writeBytes(20, 24, 28, 0) # 0 is end-of-list. 248 | 249 | # Reset text formatting parameters. 250 | def setDefault(self): 251 | self.online() 252 | self.justify('L') 253 | self.inverseOff() 254 | self.doubleHeightOff() 255 | self.setLineHeight(30) 256 | self.boldOff() 257 | self.underlineOff() 258 | self.setBarcodeHeight(50) 259 | self.setSize('s') 260 | self.setCharset() 261 | self.setCodePage() 262 | 263 | def test(self): 264 | self.write("Hello world!".encode('cp437', 'ignore')) 265 | self.feed(2) 266 | 267 | def testPage(self): 268 | self.writeBytes(18, 84) 269 | self.timeoutSet( 270 | self.dotPrintTime * 24 * 26 + 271 | self.dotFeedTime * (6 * 26 + 30)) 272 | 273 | def setBarcodeHeight(self, val=50): 274 | if val < 1: val = 1 275 | self.barcodeHeight = val 276 | self.writeBytes(29, 104, val) 277 | 278 | UPC_A = 0 279 | UPC_E = 1 280 | EAN13 = 2 281 | EAN8 = 3 282 | CODE39 = 4 283 | I25 = 5 284 | CODEBAR = 6 285 | CODE93 = 7 286 | CODE128 = 8 287 | CODE11 = 9 288 | MSI = 10 289 | ITF = 11 290 | CODABAR = 12 291 | 292 | def printBarcode(self, text, type): 293 | 294 | newDict = { # UPC codes & values for firmwareVersion >= 264 295 | self.UPC_A : 65, 296 | self.UPC_E : 66, 297 | self.EAN13 : 67, 298 | self.EAN8 : 68, 299 | self.CODE39 : 69, 300 | self.ITF : 70, 301 | self.CODABAR : 71, 302 | self.CODE93 : 72, 303 | self.CODE128 : 73, 304 | self.I25 : -1, # NOT IN NEW FIRMWARE 305 | self.CODEBAR : -1, 306 | self.CODE11 : -1, 307 | self.MSI : -1 308 | } 309 | oldDict = { # UPC codes & values for firmwareVersion < 264 310 | self.UPC_A : 0, 311 | self.UPC_E : 1, 312 | self.EAN13 : 2, 313 | self.EAN8 : 3, 314 | self.CODE39 : 4, 315 | self.I25 : 5, 316 | self.CODEBAR : 6, 317 | self.CODE93 : 7, 318 | self.CODE128 : 8, 319 | self.CODE11 : 9, 320 | self.MSI : 10, 321 | self.ITF : -1, # NOT IN OLD FIRMWARE 322 | self.CODABAR : -1 323 | } 324 | 325 | if self.firmwareVersion >= 264: 326 | n = newDict[type] 327 | else: 328 | n = oldDict[type] 329 | if n == -1: return 330 | self.feed(1) # Recent firmware requires this? 331 | self.writeBytes( 332 | 29, 72, 2, # Print label below barcode 333 | 29, 119, 3, # Barcode width 334 | 29, 107, n) # Barcode type 335 | self.timeoutWait() 336 | self.timeoutSet((self.barcodeHeight + 40) * self.dotPrintTime) 337 | # Print string 338 | if self.firmwareVersion >= 264: 339 | # Recent firmware: write length byte + string sans NUL 340 | n = len(text) 341 | if n > 255: n = 255 342 | if self.writeToStdout: 343 | sys.stdout.write((chr(n)).encode('cp437', 'ignore')) 344 | for i in range(n): 345 | sys.stdout.write(text[i].encode('utf-8', 'ignore')) 346 | else: 347 | super(Adafruit_Thermal, self).write((chr(n)).encode('utf-8', 'ignore')) 348 | for i in range(n): 349 | super(Adafruit_Thermal, 350 | self).write(text[i].encode('utf-8', 'ignore')) 351 | else: 352 | # Older firmware: write string + NUL 353 | if self.writeToStdout: 354 | sys.stdout.write(text.encode('utf-8', 'ignore')) 355 | else: 356 | super(Adafruit_Thermal, self).write(text.encode('utf-8', 'ignore')) 357 | self.prevByte = '\n' 358 | 359 | # === Character commands === 360 | 361 | INVERSE_MASK = (1 << 1) # Not in 2.6.8 firmware (see inverseOn()) 362 | UPDOWN_MASK = (1 << 2) 363 | BOLD_MASK = (1 << 3) 364 | DOUBLE_HEIGHT_MASK = (1 << 4) 365 | DOUBLE_WIDTH_MASK = (1 << 5) 366 | STRIKE_MASK = (1 << 6) 367 | 368 | def setPrintMode(self, mask): 369 | self.printMode |= mask 370 | self.writePrintMode() 371 | if self.printMode & self.DOUBLE_HEIGHT_MASK: 372 | self.charHeight = 48 373 | else: 374 | self.charHeight = 24 375 | if self.printMode & self.DOUBLE_WIDTH_MASK: 376 | self.maxColumn = 16 377 | else: 378 | self.maxColumn = 32 379 | 380 | def unsetPrintMode(self, mask): 381 | self.printMode &= ~mask 382 | self.writePrintMode() 383 | if self.printMode & self.DOUBLE_HEIGHT_MASK: 384 | self.charHeight = 48 385 | else: 386 | self.charHeight = 24 387 | if self.printMode & self.DOUBLE_WIDTH_MASK: 388 | self.maxColumn = 16 389 | else: 390 | self.maxColumn = 32 391 | 392 | def writePrintMode(self): 393 | self.writeBytes(27, 33, self.printMode) 394 | 395 | def normal(self): 396 | self.printMode = 0 397 | self.writePrintMode() 398 | 399 | def inverseOn(self): 400 | if self.firmwareVersion >= 268: 401 | self.writeBytes(29, 66, 1) 402 | else: 403 | self.setPrintMode(self.INVERSE_MASK) 404 | 405 | def inverseOff(self): 406 | if self.firmwareVersion >= 268: 407 | self.writeBytes(29, 66, 0) 408 | else: 409 | self.unsetPrintMode(self.INVERSE_MASK) 410 | 411 | def upsideDownOn(self): 412 | self.setPrintMode(self.UPDOWN_MASK) 413 | 414 | def upsideDownOff(self): 415 | self.unsetPrintMode(self.UPDOWN_MASK) 416 | 417 | def doubleHeightOn(self): 418 | self.setPrintMode(self.DOUBLE_HEIGHT_MASK) 419 | 420 | def doubleHeightOff(self): 421 | self.unsetPrintMode(self.DOUBLE_HEIGHT_MASK) 422 | 423 | def doubleWidthOn(self): 424 | self.setPrintMode(self.DOUBLE_WIDTH_MASK) 425 | 426 | def doubleWidthOff(self): 427 | self.unsetPrintMode(self.DOUBLE_WIDTH_MASK) 428 | 429 | def strikeOn(self): 430 | self.setPrintMode(self.STRIKE_MASK) 431 | 432 | def strikeOff(self): 433 | self.unsetPrintMode(self.STRIKE_MASK) 434 | 435 | def boldOn(self): 436 | self.setPrintMode(self.BOLD_MASK) 437 | 438 | def boldOff(self): 439 | self.unsetPrintMode(self.BOLD_MASK) 440 | 441 | def justify(self, value): 442 | c = value.upper() 443 | if c == 'C': 444 | pos = 1 445 | elif c == 'R': 446 | pos = 2 447 | else: 448 | pos = 0 449 | self.writeBytes(0x1B, 0x61, pos) 450 | 451 | # Feeds by the specified number of lines 452 | def feed(self, x=1): 453 | if self.firmwareVersion >= 264: 454 | self.writeBytes(27, 100, x) 455 | self.timeoutSet(self.dotFeedTime * self.charHeight) 456 | self.prevByte = '\n' 457 | self.column = 0 458 | 459 | else: 460 | # datasheet claims sending bytes 27, 100, works, 461 | # but it feeds much more than that. So, manually: 462 | while x > 0: 463 | self.write('\n'.encode('cp437', 'ignore')) 464 | x -= 1 465 | 466 | # Feeds by the specified number of individual pixel rows 467 | def feedRows(self, rows): 468 | self.writeBytes(27, 74, rows) 469 | self.timeoutSet(rows * dotFeedTime) 470 | self.prevByte = '\n' 471 | self.column = 0 472 | 473 | def flush(self): 474 | self.writeBytes(12) # ASCII FF 475 | 476 | def setSize(self, value): 477 | c = value.upper() 478 | if c == 'L': # Large: double width and height 479 | size = 0x11 480 | self.charHeight = 48 481 | self.maxColumn = 16 482 | elif c == 'M': # Medium: double height 483 | size = 0x01 484 | self.charHeight = 48 485 | self.maxColumn = 32 486 | else: # Small: standard width and height 487 | size = 0x00 488 | self.charHeight = 24 489 | self.maxColumn = 32 490 | 491 | self.writeBytes(29, 33, size) 492 | prevByte = '\n' # Setting the size adds a linefeed 493 | 494 | # Underlines of different weights can be produced: 495 | # 0 - no underline 496 | # 1 - normal underline 497 | # 2 - thick underline 498 | def underlineOn(self, weight=1): 499 | if weight > 2: weight = 2 500 | self.writeBytes(27, 45, weight) 501 | 502 | def underlineOff(self): 503 | self.writeBytes(27, 45, 0) 504 | 505 | def printBitmap(self, w, h, bitmap, LaaT=False): 506 | rowBytes = math.floor((w + 7) / 8) # Round up to next byte boundary 507 | if rowBytes >= 48: 508 | rowBytesClipped = 48 # 384 pixels max width 509 | else: 510 | rowBytesClipped = rowBytes 511 | 512 | # if LaaT (line-at-a-time) is True, print bitmaps 513 | # scanline-at-a-time (rather than in chunks). 514 | # This tends to make for much cleaner printing 515 | # (no feed gaps) on large images...but has the 516 | # opposite effect on small images that would fit 517 | # in a single 'chunk', so use carefully! 518 | if LaaT: maxChunkHeight = 1 519 | else: maxChunkHeight = 255 520 | 521 | i = 0 522 | for rowStart in range(0, h, maxChunkHeight): 523 | chunkHeight = h - rowStart 524 | if chunkHeight > maxChunkHeight: 525 | chunkHeight = maxChunkHeight 526 | 527 | # Timeout wait happens here 528 | self.writeBytes(18, 42, chunkHeight, rowBytesClipped) 529 | 530 | for y in range(chunkHeight): 531 | for x in range(rowBytesClipped): 532 | if self.writeToStdout: 533 | sys.stdout.write(bytes([bitmap[i]])) 534 | else: 535 | super(Adafruit_Thermal, 536 | self).write(bytes([bitmap[i]])) 537 | i += 1 538 | i += rowBytes - rowBytesClipped 539 | self.timeoutSet(chunkHeight * self.dotPrintTime) 540 | 541 | self.prevByte = '\n' 542 | 543 | # Print Image. Requires Python Imaging Library. This is 544 | # specific to the Python port and not present in the Arduino 545 | # library. Image will be cropped to 384 pixels width if 546 | # necessary, and converted to 1-bit w/diffusion dithering. 547 | # For any other behavior (scale, B&W threshold, etc.), use 548 | # the Imaging Library to perform such operations before 549 | # passing the result to this function. 550 | def printImage(self, image_file, LaaT=False): 551 | from PIL import Image 552 | # image = Image.open(image_file) 553 | image = image_file 554 | if image.mode != '1': 555 | image = image.convert('1') 556 | 557 | width = image.size[0] 558 | height = image.size[1] 559 | if width > 384: 560 | width = 384 561 | rowBytes = math.floor((width + 7) / 8) 562 | bitmap = bytearray(rowBytes * height) 563 | pixels = image.load() 564 | 565 | for y in range(height): 566 | n = y * rowBytes 567 | x = 0 568 | for b in range(rowBytes): 569 | sum = 0 570 | bit = 128 571 | while bit > 0: 572 | if x >= width: break 573 | if pixels[x, y] == 0: 574 | sum |= bit 575 | x += 1 576 | bit >>= 1 577 | bitmap[n + b] = sum 578 | 579 | self.printBitmap(width, height, bitmap, LaaT) 580 | 581 | # Take the printer offline. Print commands sent after this 582 | # will be ignored until 'online' is called. 583 | def offline(self): 584 | self.writeBytes(27, 61, 0) 585 | 586 | # Take the printer online. Subsequent print commands will be obeyed. 587 | def online(self): 588 | self.writeBytes(27, 61, 1) 589 | 590 | # Put the printer into a low-energy state immediately. 591 | def sleep(self): 592 | self.sleepAfter(1) # Can't be 0, that means "don't sleep" 593 | 594 | # Put the printer into a low-energy state after 595 | # the given number of seconds. 596 | def sleepAfter(self, seconds): 597 | if self.firmwareVersion >= 264: 598 | self.writeBytes(27, 56, seconds & 0xFF, seconds >> 8) 599 | else: 600 | self.writeBytes(27, 56, seconds) 601 | 602 | def wake(self): 603 | self.timeoutSet(0) 604 | self.writeBytes(255) 605 | if self.firmwareVersion >= 264: 606 | time.sleep(0.05) # 50 ms 607 | self.writeBytes(27, 118, 0) # Sleep off (important!) 608 | else: 609 | for i in range(10): 610 | self.writeBytes(27) 611 | self.timeoutSet(0.1) 612 | 613 | # Empty method, included for compatibility 614 | # with existing code ported from Arduino. 615 | def listen(self): 616 | pass 617 | 618 | # Check the status of the paper using the printers self reporting 619 | # ability. Doesn't match the datasheet... 620 | # Returns True for paper, False for no paper. 621 | def hasPaper(self): 622 | if self.firmwareVersion >= 264: 623 | self.writeBytes(27, 118, 0) 624 | else: 625 | self.writeBytes(29, 114, 0) 626 | # Bit 2 of response seems to be paper status 627 | stat = ord(self.read(1)) & 0b00000100 628 | # If set, we have paper; if clear, no paper 629 | return stat == 0 630 | 631 | def setLineHeight(self, val=32): 632 | if val < 24: val = 24 633 | self.lineSpacing = val - 24 634 | 635 | # The printer doesn't take into account the current text 636 | # height when setting line height, making this more akin 637 | # to inter-line spacing. Default line spacing is 32 638 | # (char height of 24, line spacing of 8). 639 | self.writeBytes(27, 51, val) 640 | 641 | CHARSET_USA = 0 642 | CHARSET_FRANCE = 1 643 | CHARSET_GERMANY = 2 644 | CHARSET_UK = 3 645 | CHARSET_DENMARK1 = 4 646 | CHARSET_SWEDEN = 5 647 | CHARSET_ITALY = 6 648 | CHARSET_SPAIN1 = 7 649 | CHARSET_JAPAN = 8 650 | CHARSET_NORWAY = 9 651 | CHARSET_DENMARK2 = 10 652 | CHARSET_SPAIN2 = 11 653 | CHARSET_LATINAMERICA = 12 654 | CHARSET_KOREA = 13 655 | CHARSET_SLOVENIA = 14 656 | CHARSET_CROATIA = 14 657 | CHARSET_CHINA = 15 658 | 659 | # Alters some chars in ASCII 0x23-0x7E range; see datasheet 660 | def setCharset(self, val=0): 661 | if val > 15: val = 15 662 | self.writeBytes(27, 82, val) 663 | 664 | CODEPAGE_CP437 = 0 # USA, Standard Europe 665 | CODEPAGE_KATAKANA = 1 666 | CODEPAGE_CP850 = 2 # Multilingual 667 | CODEPAGE_CP860 = 3 # Portuguese 668 | CODEPAGE_CP863 = 4 # Canadian-French 669 | CODEPAGE_CP865 = 5 # Nordic 670 | CODEPAGE_WCP1251 = 6 # Cyrillic 671 | CODEPAGE_CP866 = 7 # Cyrillic #2 672 | CODEPAGE_MIK = 8 # Cyrillic/Bulgarian 673 | CODEPAGE_CP755 = 9 # East Europe, Latvian 2 674 | CODEPAGE_IRAN = 10 675 | CODEPAGE_CP862 = 15 # Hebrew 676 | CODEPAGE_WCP1252 = 16 # Latin 1 677 | CODEPAGE_WCP1253 = 17 # Greek 678 | CODEPAGE_CP852 = 18 # Latin 2 679 | CODEPAGE_CP858 = 19 # Multilingual Latin 1 + Euro 680 | CODEPAGE_IRAN2 = 20 681 | CODEPAGE_LATVIAN = 21 682 | CODEPAGE_CP864 = 22 # Arabic 683 | CODEPAGE_ISO_8859_1 = 23 # West Europe 684 | CODEPAGE_CP737 = 24 # Greek 685 | CODEPAGE_WCP1257 = 25 # Baltic 686 | CODEPAGE_THAI = 26 687 | CODEPAGE_CP720 = 27 # Arabic 688 | CODEPAGE_CP855 = 28 689 | CODEPAGE_CP857 = 29 # Turkish 690 | CODEPAGE_WCP1250 = 30 # Central Europe 691 | CODEPAGE_CP775 = 31 692 | CODEPAGE_WCP1254 = 32 # Turkish 693 | CODEPAGE_WCP1255 = 33 # Hebrew 694 | CODEPAGE_WCP1256 = 34 # Arabic 695 | CODEPAGE_WCP1258 = 35 # Vietnam 696 | CODEPAGE_ISO_8859_2 = 36 # Latin 2 697 | CODEPAGE_ISO_8859_3 = 37 # Latin 3 698 | CODEPAGE_ISO_8859_4 = 38 # Baltic 699 | CODEPAGE_ISO_8859_5 = 39 # Cyrillic 700 | CODEPAGE_ISO_8859_6 = 40 # Arabic 701 | CODEPAGE_ISO_8859_7 = 41 # Greek 702 | CODEPAGE_ISO_8859_8 = 42 # Hebrew 703 | CODEPAGE_ISO_8859_9 = 43 # Turkish 704 | CODEPAGE_ISO_8859_15 = 44 # Latin 3 705 | CODEPAGE_THAI2 = 45 706 | CODEPAGE_CP856 = 46 707 | CODEPAGE_CP874 = 47 708 | 709 | # Selects alt symbols for 'upper' ASCII values 0x80-0xFF 710 | def setCodePage(self, val=0): 711 | if val > 47: val = 47 712 | self.writeBytes(27, 116, val) 713 | 714 | # Copied from Arduino lib for parity; may not work on all printers 715 | def tab(self): 716 | self.writeBytes(9) 717 | self.column = (self.column + 4) & 0xFC 718 | 719 | # Copied from Arduino lib for parity; may not work on all printers 720 | def setCharSpacing(self, spacing): 721 | self.writeBytes(27, 32, spacing) 722 | 723 | # Overloading print() in Python pre-3.0 is dirty pool, 724 | # but these are here to provide more direct compatibility 725 | # with existing code written for the Arduino library. 726 | def print(self, *args, **kwargs): 727 | for arg in args: 728 | self.write((str(arg)).encode('cp437', 'ignore')) 729 | 730 | # For Arduino code compatibility again 731 | def println(self, *args, **kwargs): 732 | for arg in args: 733 | self.write((str(arg)).encode('cp437', 'ignore')) 734 | self.write('\n'.encode('cp437', 'ignore')) 735 | 736 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # !!! NOTE !!! 2 | # THIS REPOSITORY IS ARCHIVED AND IS NO LONGER SUPPORTED OR MAINTAINED 3 | 4 | # Python-Thermal-Printer Module 5 | 6 | Python3 port of the original Adafruit [Python-Thermal-Printer](https://github.com/adafruit/Python-Thermal-Printer) library. 7 | 8 | ## Getting Started 9 | 10 | 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. 11 | 12 | Run a test to see if the printer is working by punching in these commands into the terminal. 13 | 14 | ``` shell 15 | stty -F /dev/serial0 19200 16 | echo -e "This is a test.\\n\\n\\n" > /dev/serial0 17 | ``` 18 | 19 | ### Installing 20 | 21 | Update the system and install prerequisites. 22 | 23 | ``` shell 24 | sudo apt-get update 25 | sudo apt-get install git cups wiringpi build-essential libcups2-dev libcupsimage2-dev python3-serial python-pil python-unidecode 26 | ``` 27 | 28 | Install the printer driver. Don't worry about the warnings that g++ gives. 29 | 30 | ``` shell 31 | git clone https://github.com/adafruit/zj-58 32 | cd zj-58 33 | make 34 | sudo ./install 35 | ``` 36 | 37 | Make the printer the default printer. This is useful if you are going to be doing other things with it. 38 | 39 | ``` shell 40 | sudo lpadmin -p ZJ-58 -E -v serial:/dev/serial0?baud=19200 -m zjiang/ZJ-58.ppd 41 | sudo lpoptions -d ZJ-58 42 | ``` 43 | 44 | Restart the system. Clone this repository and try to run *printertest.py*. 45 | 46 | ``` shell 47 | git clone https://github.com/galacticfan/Python-Thermal-Printer/ 48 | cd Python-Thermal-Printer 49 | python3 printertest.py 50 | ``` 51 | 52 | Let me know if you have any issues. 53 | -------------------------------------------------------------------------------- /calibrate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Thermal calibration utility for Adafruit_Thermal Python library. 4 | # Run this utility before using the printer for the first time, any 5 | # time a different power supply is used, or when using paper from a 6 | # different source. 7 | # 8 | # Prints a series of black bars with increasing "heat time" settings. 9 | # Because printed sections have different "grip" characteristics than 10 | # blank paper, as this progresses the paper will usually at some point 11 | # jam -- either uniformly, making a short bar, or at one side or the 12 | # other, making a wedge shape. In some cases, the Pi may reset for 13 | # lack of power. 14 | # 15 | # Whatever the outcome, take the last number printed BEFORE any 16 | # distorted bar and enter in in Adafruit_Thermal.py as defaultHeatTime 17 | # (around line 53). 18 | # 19 | # You may need to pull on the paper as it reaches the jamming point, 20 | # and/or just abort the program, press the feed button and take the 21 | # last good number. 22 | 23 | from __future__ import print_function 24 | from Adafruit_Thermal import * 25 | 26 | printer = Adafruit_Thermal("/dev/serial0", 19200, timeout=5) 27 | 28 | for i in range(0,256,15): 29 | printer.begin(i) 30 | printer.println(i) # Print heat time 31 | printer.inverseOn() 32 | printer.print('{:^32}'.format('')) # Print 32 spaces (inverted) 33 | printer.inverseOff() 34 | 35 | printer.begin() # Reset heat time to default 36 | printer.feed(4) 37 | -------------------------------------------------------------------------------- /datetime.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Python3 script to print the current date and time, using 4 | # the Adafruit_Thermal library, in ISO 8601 format. 5 | # https://www.iso.org/iso-8601-date-and-time-format.html 6 | 7 | import time 8 | from Adafruit_Thermal import * 9 | 10 | # Lines of margin (integer) 11 | i_feed = 3 12 | # Seconds to pause (float) 13 | f_pause = 1.0 14 | 15 | # Define the printer port, speed, and timeout 16 | printer = Adafruit_Thermal("/dev/ttyS0", 19200, timeout=5) 17 | 18 | # Build the date stamp in the format YYYY-MM-DD ex: "2021-12-25" 19 | datestamp = time.strftime("%Y-%m-%d", time.gmtime()) 20 | print ("Date in preferred format:", datestamp) 21 | 22 | # Build the time stamp in the format Thh:mm:ssZ ex: "T23:59:59Z" 23 | timestamp = 'T' + time.strftime("%H:%M:%S", time.gmtime()) + 'Z' 24 | print ("Time in preferred format:", timestamp) 25 | 26 | # Tell printer to sleep 27 | printer.sleep() 28 | # Sleep for the defined time in case we're called many times in a row 29 | time.sleep(f_pause) 30 | # Call wake() before printing again, even if reset 31 | printer.wake() 32 | # Restore printer to defaults 33 | printer.setDefault() 34 | 35 | # Give a little room at the top 36 | printer.feed(i_feed) 37 | # Center justify 38 | printer.justify('C') 39 | # Large size 40 | printer.setSize('L') 41 | # Print the date 42 | printer.println(datestamp) 43 | # Print the time 44 | printer.println(timestamp) 45 | # Give a little room at the bottom 46 | printer.feed(i_feed) 47 | -------------------------------------------------------------------------------- /forecast.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from Adafruit_Thermal import * 3 | from datetime import date 4 | import calendar 5 | import urllib.request 6 | import json 7 | 8 | printer = Adafruit_Thermal("/dev/serial0", 19200, timeout=5) 9 | def getLink(dailyOrHourly): 10 | latitude = "38.8894" #limit to four decimal digits 11 | longitude = "-77.0352" #limit to four decimal digits 12 | mainLink = "https://api.weather.gov/points/" + latitude + "," + longitude 13 | response_main = urllib.request.urlopen(mainLink) 14 | raw_data_main = response_main.read().decode() 15 | data_main = json.loads(raw_data_main) 16 | properties_main = data_main['properties'] 17 | dailyLink = properties_main["forecast"] 18 | hourlyLink = properties_main["forecastHourly"] 19 | if dailyOrHourly == "daily": 20 | return dailyLink 21 | elif dailyOrHourly == "hourly": 22 | return hourlyLink 23 | 24 | url_daily = getLink("daily") 25 | response_daily = urllib.request.urlopen(url_daily) 26 | # status & reason 27 | # print(response_daily.status, response_daily.reason) 28 | 29 | raw_data_daily = response_daily.read().decode() 30 | data_daily = json.loads(raw_data_daily) 31 | forecast_periods_daily = data_daily['properties']['periods'] 32 | 33 | 34 | current_period_isDayTime = forecast_periods_daily[0]['isDaytime'] 35 | 36 | if current_period_isDayTime: 37 | day_index = 0 38 | night_index = 1 39 | else: 40 | day_index = 1 41 | night_index = 0 42 | 43 | day_name = forecast_periods_daily[day_index]['name'] 44 | hi_temp = forecast_periods_daily[day_index]['temperature'] 45 | night_name = forecast_periods_daily[night_index]['name'] 46 | lo_temp = forecast_periods_daily[night_index]['temperature'] 47 | current_detailed_forecast = forecast_periods_daily[0]['detailedForecast'] 48 | 49 | url_hourly = getLink("hourly") 50 | response_hourly = urllib.request.urlopen(url_hourly) 51 | # status & reason 52 | #print(response_hourly.status, response_hourly.reason) 53 | 54 | raw_data_hourly = response_hourly.read().decode() 55 | data_hourly = json.loads(raw_data_hourly) 56 | forecast_periods_hourly = data_hourly['properties']['periods'] 57 | temperature = forecast_periods_hourly[0]['temperature'] 58 | 59 | d = date.today() 60 | week_day = calendar.day_name[date(d.year,d.month,d.day).weekday()] 61 | month_text = calendar.month_name[d.month] 62 | printer.underlineOn() 63 | printer.print("It's " + week_day + ", " + month_text + " " + str(d.day) + "\n") 64 | printer.underlineOff() 65 | printer.boldOn() 66 | printer.print(day_name + "'s Forecast \n") 67 | printer.boldOff() 68 | printer.print("Current temperature: " + str(temperature) + " F \n") 69 | printer.print("High temperature: " + str(hi_temp) + " F \n") 70 | printer.print("Low temperature: " + str(lo_temp) + " F \n") 71 | printer.print(current_detailed_forecast + "\n") 72 | printer.feed(3) 73 | -------------------------------------------------------------------------------- /gfx/__init__.py: -------------------------------------------------------------------------------- 1 | # No code here; it's only present so imports in printertest work. 2 | -------------------------------------------------------------------------------- /gfx/adalogo.py: -------------------------------------------------------------------------------- 1 | width = 75 2 | height = 75 3 | data = [ 4 | 0x00,0x00,0x00,0x00,0x00,0xe0,0x00,0x00,0x00,0x00, 5 | 0x00,0x00,0x00,0x00,0x01,0xf0,0x00,0x00,0x00,0x00, 6 | 0x00,0x00,0x00,0x00,0x03,0xf0,0x00,0x00,0x00,0x00, 7 | 0x00,0x00,0x00,0x00,0x03,0xf8,0x00,0x00,0x00,0x00, 8 | 0x00,0x00,0x00,0x00,0x07,0xf8,0x00,0x00,0x00,0x00, 9 | 0x00,0x00,0x00,0x00,0x0f,0xf8,0x00,0x00,0x00,0x00, 10 | 0x00,0x00,0x00,0x00,0x1f,0xfc,0x00,0x00,0x00,0x00, 11 | 0x00,0x00,0x00,0x00,0x1f,0xfc,0x00,0x00,0x00,0x00, 12 | 0x00,0x00,0x00,0x00,0x3f,0xfc,0x00,0x00,0x00,0x00, 13 | 0x00,0x00,0x00,0x00,0x7f,0xfe,0x00,0x00,0x00,0x00, 14 | 0x00,0x00,0x00,0x00,0x7f,0xfe,0x00,0x00,0x00,0x00, 15 | 0x00,0x00,0x00,0x00,0xff,0xfe,0x00,0x00,0x00,0x00, 16 | 0x00,0x00,0x00,0x01,0xff,0xff,0x00,0x00,0x00,0x00, 17 | 0x00,0x00,0x00,0x03,0xff,0xff,0x00,0x00,0x00,0x00, 18 | 0x00,0x00,0x00,0x03,0xff,0xff,0x00,0x00,0x00,0x00, 19 | 0x00,0x00,0x00,0x07,0xff,0xff,0x80,0x00,0x00,0x00, 20 | 0x00,0x00,0x00,0x07,0xff,0xff,0x80,0x00,0x00,0x00, 21 | 0x00,0x00,0x00,0x07,0xff,0xff,0x80,0x00,0x00,0x00, 22 | 0x00,0x00,0x00,0x0f,0xff,0xff,0x80,0x00,0x00,0x00, 23 | 0x00,0x00,0x00,0x0f,0xff,0xff,0x80,0x00,0x00,0x00, 24 | 0x7f,0xff,0xfc,0x0f,0xff,0xff,0x80,0x00,0x00,0x00, 25 | 0xff,0xff,0xff,0x0f,0xff,0xff,0x80,0x00,0x00,0x00, 26 | 0xff,0xff,0xff,0xcf,0xff,0xff,0x80,0x00,0x00,0x00, 27 | 0xff,0xff,0xff,0xef,0xff,0xff,0x80,0x00,0x00,0x00, 28 | 0x7f,0xff,0xff,0xf7,0xff,0xff,0x80,0x00,0x00,0x00, 29 | 0x3f,0xff,0xff,0xff,0xfb,0xff,0x00,0x00,0x00,0x00, 30 | 0x3f,0xff,0xff,0xff,0xf1,0xff,0x3f,0xf0,0x00,0x00, 31 | 0x1f,0xff,0xff,0xff,0xf1,0xfe,0xff,0xfe,0x00,0x00, 32 | 0x0f,0xff,0xff,0xff,0xf1,0xff,0xff,0xff,0xc0,0x00, 33 | 0x0f,0xff,0xff,0xff,0xe1,0xff,0xff,0xff,0xf8,0x00, 34 | 0x07,0xff,0xff,0xff,0xe1,0xff,0xff,0xff,0xff,0x00, 35 | 0x03,0xff,0xff,0xff,0xe1,0xff,0xff,0xff,0xff,0xc0, 36 | 0x01,0xff,0xff,0x3f,0xe1,0xff,0xff,0xff,0xff,0xe0, 37 | 0x01,0xff,0xfe,0x07,0xe3,0xff,0xff,0xff,0xff,0xe0, 38 | 0x00,0xff,0xff,0x03,0xe3,0xff,0xff,0xff,0xff,0xe0, 39 | 0x00,0x7f,0xff,0x00,0xf7,0xff,0xff,0xff,0xff,0xc0, 40 | 0x00,0x3f,0xff,0xc0,0xff,0xc0,0x7f,0xff,0xff,0x80, 41 | 0x00,0x1f,0xff,0xf0,0xff,0x00,0x3f,0xff,0xff,0x00, 42 | 0x00,0x0f,0xff,0xff,0xff,0x00,0x7f,0xff,0xfc,0x00, 43 | 0x00,0x07,0xff,0xff,0xff,0x01,0xff,0xff,0xf8,0x00, 44 | 0x00,0x01,0xff,0xff,0xff,0xff,0xff,0xff,0xf0,0x00, 45 | 0x00,0x00,0x7f,0xff,0xff,0xff,0xff,0xff,0xc0,0x00, 46 | 0x00,0x00,0x1f,0xfc,0x7f,0xff,0xff,0xff,0x80,0x00, 47 | 0x00,0x00,0x7f,0xf8,0x78,0xff,0xff,0xfe,0x00,0x00, 48 | 0x00,0x00,0xff,0xf0,0x78,0x7f,0xff,0xfc,0x00,0x00, 49 | 0x00,0x01,0xff,0xe0,0xf8,0x7f,0xff,0xf0,0x00,0x00, 50 | 0x00,0x03,0xff,0xc0,0xf8,0x3f,0xdf,0xc0,0x00,0x00, 51 | 0x00,0x07,0xff,0xc1,0xfc,0x3f,0xe0,0x00,0x00,0x00, 52 | 0x00,0x07,0xff,0x87,0xfc,0x1f,0xf0,0x00,0x00,0x00, 53 | 0x00,0x0f,0xff,0xcf,0xfe,0x1f,0xf8,0x00,0x00,0x00, 54 | 0x00,0x0f,0xff,0xff,0xff,0x1f,0xf8,0x00,0x00,0x00, 55 | 0x00,0x1f,0xff,0xff,0xff,0x1f,0xfc,0x00,0x00,0x00, 56 | 0x00,0x1f,0xff,0xff,0xff,0xff,0xfc,0x00,0x00,0x00, 57 | 0x00,0x1f,0xff,0xff,0xff,0xff,0xfe,0x00,0x00,0x00, 58 | 0x00,0x3f,0xff,0xff,0xff,0xff,0xfe,0x00,0x00,0x00, 59 | 0x00,0x3f,0xff,0xff,0xff,0xff,0xfe,0x00,0x00,0x00, 60 | 0x00,0x3f,0xff,0xff,0x3f,0xff,0xfe,0x00,0x00,0x00, 61 | 0x00,0x7f,0xff,0xff,0x3f,0xff,0xfe,0x00,0x00,0x00, 62 | 0x00,0x7f,0xff,0xff,0x3f,0xff,0xfe,0x00,0x00,0x00, 63 | 0x00,0x7f,0xff,0xfe,0x3f,0xff,0xfe,0x00,0x00,0x00, 64 | 0x00,0xff,0xff,0xfc,0x1f,0xff,0xfe,0x00,0x00,0x00, 65 | 0x00,0xff,0xff,0xf8,0x1f,0xff,0xfe,0x00,0x00,0x00, 66 | 0x00,0xff,0xff,0xe0,0x0f,0xff,0xfe,0x00,0x00,0x00, 67 | 0x01,0xff,0xff,0x80,0x07,0xff,0xfe,0x00,0x00,0x00, 68 | 0x01,0xff,0xfc,0x00,0x03,0xff,0xfe,0x00,0x00,0x00, 69 | 0x01,0xff,0xe0,0x00,0x01,0xff,0xfe,0x00,0x00,0x00, 70 | 0x01,0xff,0x00,0x00,0x00,0xff,0xfe,0x00,0x00,0x00, 71 | 0x00,0xf8,0x00,0x00,0x00,0x7f,0xfe,0x00,0x00,0x00, 72 | 0x00,0x00,0x00,0x00,0x00,0x1f,0xfe,0x00,0x00,0x00, 73 | 0x00,0x00,0x00,0x00,0x00,0x0f,0xfe,0x00,0x00,0x00, 74 | 0x00,0x00,0x00,0x00,0x00,0x07,0xfe,0x00,0x00,0x00, 75 | 0x00,0x00,0x00,0x00,0x00,0x01,0xfe,0x00,0x00,0x00, 76 | 0x00,0x00,0x00,0x00,0x00,0x00,0xfe,0x00,0x00,0x00, 77 | 0x00,0x00,0x00,0x00,0x00,0x00,0x7e,0x00,0x00,0x00, 78 | 0x00,0x00,0x00,0x00,0x00,0x00,0x1c,0x00,0x00,0x00 79 | ] 80 | -------------------------------------------------------------------------------- /gfx/adaqrcode.py: -------------------------------------------------------------------------------- 1 | width = 135 2 | height = 135 3 | data = [ 4 | 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 5 | 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 6 | 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 7 | 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 8 | 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 9 | 0x07,0xff,0xff,0xff,0xff,0x00,0x01,0xff,0xff,0xff,0xf8,0x01,0xff,0xff,0xff,0xff,0xc0, 10 | 0x07,0xff,0xff,0xff,0xff,0x00,0x01,0xff,0xff,0xff,0xf8,0x01,0xff,0xff,0xff,0xff,0xc0, 11 | 0x07,0xff,0xff,0xff,0xff,0x00,0x01,0xff,0xff,0xff,0xf8,0x01,0xff,0xff,0xff,0xff,0xc0, 12 | 0x07,0xff,0xff,0xff,0xff,0x00,0x01,0xff,0xff,0xff,0xf8,0x01,0xff,0xff,0xff,0xff,0xc0, 13 | 0x07,0xff,0xff,0xff,0xff,0x00,0x01,0xff,0xff,0xff,0xf8,0x01,0xff,0xff,0xff,0xff,0xc0, 14 | 0x07,0xc0,0x00,0x00,0x1f,0x00,0x3f,0xff,0x80,0x00,0xf8,0x01,0xf0,0x00,0x00,0x07,0xc0, 15 | 0x07,0xc0,0x00,0x00,0x1f,0x00,0x3f,0xff,0x80,0x00,0xf8,0x01,0xf0,0x00,0x00,0x07,0xc0, 16 | 0x07,0xc0,0x00,0x00,0x1f,0x00,0x3f,0xff,0x80,0x00,0xf8,0x01,0xf0,0x00,0x00,0x07,0xc0, 17 | 0x07,0xc0,0x00,0x00,0x1f,0x00,0x3f,0xff,0x80,0x00,0xf8,0x01,0xf0,0x00,0x00,0x07,0xc0, 18 | 0x07,0xc0,0x00,0x00,0x1f,0x00,0x3f,0xff,0x80,0x00,0xf8,0x01,0xf0,0x00,0x00,0x07,0xc0, 19 | 0x07,0xc1,0xff,0xfc,0x1f,0x00,0x3e,0x0f,0xff,0xe0,0x00,0x01,0xf0,0x7f,0xff,0x07,0xc0, 20 | 0x07,0xc1,0xff,0xfc,0x1f,0x00,0x3e,0x0f,0xff,0xe0,0x00,0x01,0xf0,0x7f,0xff,0x07,0xc0, 21 | 0x07,0xc1,0xff,0xfc,0x1f,0x00,0x3e,0x0f,0xff,0xe0,0x00,0x01,0xf0,0x7f,0xff,0x07,0xc0, 22 | 0x07,0xc1,0xff,0xfc,0x1f,0x00,0x3e,0x0f,0xff,0xe0,0x00,0x01,0xf0,0x7f,0xff,0x07,0xc0, 23 | 0x07,0xc1,0xff,0xfc,0x1f,0x00,0x3e,0x0f,0xff,0xe0,0x00,0x01,0xf0,0x7f,0xff,0x07,0xc0, 24 | 0x07,0xc1,0xff,0xfc,0x1f,0x07,0xff,0xf0,0x7c,0x00,0xf8,0x01,0xf0,0x7f,0xff,0x07,0xc0, 25 | 0x07,0xc1,0xff,0xfc,0x1f,0x07,0xff,0xf0,0x7c,0x00,0xf8,0x01,0xf0,0x7f,0xff,0x07,0xc0, 26 | 0x07,0xc1,0xff,0xfc,0x1f,0x07,0xff,0xf0,0x7c,0x00,0xf8,0x01,0xf0,0x7f,0xff,0x07,0xc0, 27 | 0x07,0xc1,0xff,0xfc,0x1f,0x07,0xff,0xf0,0x7c,0x00,0xf8,0x01,0xf0,0x7f,0xff,0x07,0xc0, 28 | 0x07,0xc1,0xff,0xfc,0x1f,0x07,0xff,0xf0,0x7c,0x00,0xf8,0x01,0xf0,0x7f,0xff,0x07,0xc0, 29 | 0x07,0xc1,0xff,0xfc,0x1f,0x00,0x3f,0xf0,0x7c,0x00,0xff,0xc1,0xf0,0x7f,0xff,0x07,0xc0, 30 | 0x07,0xc1,0xff,0xfc,0x1f,0x00,0x3f,0xf0,0x7c,0x00,0xff,0xc1,0xf0,0x7f,0xff,0x07,0xc0, 31 | 0x07,0xc1,0xff,0xfc,0x1f,0x00,0x3f,0xf0,0x7c,0x00,0xff,0xc1,0xf0,0x7f,0xff,0x07,0xc0, 32 | 0x07,0xc1,0xff,0xfc,0x1f,0x00,0x3f,0xf0,0x7c,0x00,0xff,0xc1,0xf0,0x7f,0xff,0x07,0xc0, 33 | 0x07,0xc1,0xff,0xfc,0x1f,0x00,0x3f,0xf0,0x7c,0x00,0xff,0xc1,0xf0,0x7f,0xff,0x07,0xc0, 34 | 0x07,0xc0,0x00,0x00,0x1f,0x07,0xff,0xf0,0x7f,0xff,0x00,0x01,0xf0,0x00,0x00,0x07,0xc0, 35 | 0x07,0xc0,0x00,0x00,0x1f,0x07,0xff,0xf0,0x7f,0xff,0x00,0x01,0xf0,0x00,0x00,0x07,0xc0, 36 | 0x07,0xc0,0x00,0x00,0x1f,0x07,0xff,0xf0,0x7f,0xff,0x00,0x01,0xf0,0x00,0x00,0x07,0xc0, 37 | 0x07,0xc0,0x00,0x00,0x1f,0x07,0xff,0xf0,0x7f,0xff,0x00,0x01,0xf0,0x00,0x00,0x07,0xc0, 38 | 0x07,0xc0,0x00,0x00,0x1f,0x07,0xff,0xf0,0x7f,0xff,0x00,0x01,0xf0,0x00,0x00,0x07,0xc0, 39 | 0x07,0xff,0xff,0xff,0xff,0x07,0xc1,0xf0,0x7c,0x1f,0x07,0xc1,0xff,0xff,0xff,0xff,0xc0, 40 | 0x07,0xff,0xff,0xff,0xff,0x07,0xc1,0xf0,0x7c,0x1f,0x07,0xc1,0xff,0xff,0xff,0xff,0xc0, 41 | 0x07,0xff,0xff,0xff,0xff,0x07,0xc1,0xf0,0x7c,0x1f,0x07,0xc1,0xff,0xff,0xff,0xff,0xc0, 42 | 0x07,0xff,0xff,0xff,0xff,0x07,0xc1,0xf0,0x7c,0x1f,0x07,0xc1,0xff,0xff,0xff,0xff,0xc0, 43 | 0x07,0xff,0xff,0xff,0xff,0x07,0xc1,0xf0,0x7c,0x1f,0x07,0xc1,0xff,0xff,0xff,0xff,0xc0, 44 | 0x00,0x00,0x00,0x00,0x00,0x07,0xc1,0xf0,0x03,0xe0,0x07,0xc0,0x00,0x00,0x00,0x00,0x00, 45 | 0x00,0x00,0x00,0x00,0x00,0x07,0xc1,0xf0,0x03,0xe0,0x07,0xc0,0x00,0x00,0x00,0x00,0x00, 46 | 0x00,0x00,0x00,0x00,0x00,0x07,0xc1,0xf0,0x03,0xe0,0x07,0xc0,0x00,0x00,0x00,0x00,0x00, 47 | 0x00,0x00,0x00,0x00,0x00,0x07,0xc1,0xf0,0x03,0xe0,0x07,0xc0,0x00,0x00,0x00,0x00,0x00, 48 | 0x00,0x00,0x00,0x00,0x00,0x07,0xc1,0xf0,0x03,0xe0,0x07,0xc0,0x00,0x00,0x00,0x00,0x00, 49 | 0x00,0x3f,0xf0,0x00,0x1f,0x00,0x01,0xf0,0x03,0xe0,0xf8,0x01,0xff,0x83,0xe0,0x00,0x00, 50 | 0x00,0x3f,0xf0,0x00,0x1f,0x00,0x01,0xf0,0x03,0xe0,0xf8,0x01,0xff,0x83,0xe0,0x00,0x00, 51 | 0x00,0x3f,0xf0,0x00,0x1f,0x00,0x01,0xf0,0x03,0xe0,0xf8,0x01,0xff,0x83,0xe0,0x00,0x00, 52 | 0x00,0x3f,0xf0,0x00,0x1f,0x00,0x01,0xf0,0x03,0xe0,0xf8,0x01,0xff,0x83,0xe0,0x00,0x00, 53 | 0x00,0x3f,0xf0,0x00,0x1f,0x00,0x01,0xf0,0x03,0xe0,0xf8,0x01,0xff,0x83,0xe0,0x00,0x00, 54 | 0x07,0xc1,0xf0,0x00,0x00,0xf8,0x3e,0x00,0x7c,0x00,0xff,0xc1,0xff,0x83,0xe0,0xff,0xc0, 55 | 0x07,0xc1,0xf0,0x00,0x00,0xf8,0x3e,0x00,0x7c,0x00,0xff,0xc1,0xff,0x83,0xe0,0xff,0xc0, 56 | 0x07,0xc1,0xf0,0x00,0x00,0xf8,0x3e,0x00,0x7c,0x00,0xff,0xc1,0xff,0x83,0xe0,0xff,0xc0, 57 | 0x07,0xc1,0xf0,0x00,0x00,0xf8,0x3e,0x00,0x7c,0x00,0xff,0xc1,0xff,0x83,0xe0,0xff,0xc0, 58 | 0x07,0xc1,0xf0,0x00,0x00,0xf8,0x3e,0x00,0x7c,0x00,0xff,0xc1,0xff,0x83,0xe0,0xff,0xc0, 59 | 0x07,0xc1,0xf0,0x03,0xff,0x07,0xc1,0xf0,0x7f,0xff,0xf8,0x3f,0xff,0xff,0xff,0x07,0xc0, 60 | 0x07,0xc1,0xf0,0x03,0xff,0x07,0xc1,0xf0,0x7f,0xff,0xf8,0x3f,0xff,0xff,0xff,0x07,0xc0, 61 | 0x07,0xc1,0xf0,0x03,0xff,0x07,0xc1,0xf0,0x7f,0xff,0xf8,0x3f,0xff,0xff,0xff,0x07,0xc0, 62 | 0x07,0xc1,0xf0,0x03,0xff,0x07,0xc1,0xf0,0x7f,0xff,0xf8,0x3f,0xff,0xff,0xff,0x07,0xc0, 63 | 0x07,0xc1,0xf0,0x03,0xff,0x07,0xc1,0xf0,0x7f,0xff,0xf8,0x3f,0xff,0xff,0xff,0x07,0xc0, 64 | 0x00,0x3e,0x0f,0xfc,0x00,0xf8,0x3e,0x00,0x7c,0x1f,0x00,0x3e,0x0f,0x83,0xe0,0x00,0x00, 65 | 0x00,0x3e,0x0f,0xfc,0x00,0xf8,0x3e,0x00,0x7c,0x1f,0x00,0x3e,0x0f,0x83,0xe0,0x00,0x00, 66 | 0x00,0x3e,0x0f,0xfc,0x00,0xf8,0x3e,0x00,0x7c,0x1f,0x00,0x3e,0x0f,0x83,0xe0,0x00,0x00, 67 | 0x00,0x3e,0x0f,0xfc,0x00,0xf8,0x3e,0x00,0x7c,0x1f,0x00,0x3e,0x0f,0x83,0xe0,0x00,0x00, 68 | 0x00,0x3e,0x0f,0xfc,0x00,0xf8,0x3e,0x00,0x7c,0x1f,0x00,0x3e,0x0f,0x83,0xe0,0x00,0x00, 69 | 0x00,0x3f,0xff,0xff,0xff,0xff,0xfe,0x0f,0x83,0xff,0x07,0xc1,0xff,0x80,0x00,0x07,0xc0, 70 | 0x00,0x3f,0xff,0xff,0xff,0xff,0xfe,0x0f,0x83,0xff,0x07,0xc1,0xff,0x80,0x00,0x07,0xc0, 71 | 0x00,0x3f,0xff,0xff,0xff,0xff,0xfe,0x0f,0x83,0xff,0x07,0xc1,0xff,0x80,0x00,0x07,0xc0, 72 | 0x00,0x3f,0xff,0xff,0xff,0xff,0xfe,0x0f,0x83,0xff,0x07,0xc1,0xff,0x80,0x00,0x07,0xc0, 73 | 0x00,0x3f,0xff,0xff,0xff,0xff,0xfe,0x0f,0x83,0xff,0x07,0xc1,0xff,0x80,0x00,0x07,0xc0, 74 | 0x00,0x3e,0x00,0x03,0xe0,0x07,0xfe,0x00,0x00,0x1f,0xff,0xc1,0xff,0x80,0x00,0xff,0xc0, 75 | 0x00,0x3e,0x00,0x03,0xe0,0x07,0xfe,0x00,0x00,0x1f,0xff,0xc1,0xff,0x80,0x00,0xff,0xc0, 76 | 0x00,0x3e,0x00,0x03,0xe0,0x07,0xfe,0x00,0x00,0x1f,0xff,0xc1,0xff,0x80,0x00,0xff,0xc0, 77 | 0x00,0x3e,0x00,0x03,0xe0,0x07,0xfe,0x00,0x00,0x1f,0xff,0xc1,0xff,0x80,0x00,0xff,0xc0, 78 | 0x00,0x3e,0x00,0x03,0xe0,0x07,0xfe,0x00,0x00,0x1f,0xff,0xc1,0xff,0x80,0x00,0xff,0xc0, 79 | 0x07,0xfe,0x00,0x7c,0x1f,0xf8,0x3e,0x0f,0x83,0xff,0xff,0xc1,0xf0,0x7f,0xff,0x07,0xc0, 80 | 0x07,0xfe,0x00,0x7c,0x1f,0xf8,0x3e,0x0f,0x83,0xff,0xff,0xc1,0xf0,0x7f,0xff,0x07,0xc0, 81 | 0x07,0xfe,0x00,0x7c,0x1f,0xf8,0x3e,0x0f,0x83,0xff,0xff,0xc1,0xf0,0x7f,0xff,0x07,0xc0, 82 | 0x07,0xfe,0x00,0x7c,0x1f,0xf8,0x3e,0x0f,0x83,0xff,0xff,0xc1,0xf0,0x7f,0xff,0x07,0xc0, 83 | 0x07,0xfe,0x00,0x7c,0x1f,0xf8,0x3e,0x0f,0x83,0xff,0xff,0xc1,0xf0,0x7f,0xff,0x07,0xc0, 84 | 0x00,0x00,0x00,0x7f,0xe0,0x00,0x3e,0x00,0x7f,0xe0,0xff,0xc1,0xf0,0x00,0x00,0x00,0x00, 85 | 0x00,0x00,0x00,0x7f,0xe0,0x00,0x3e,0x00,0x7f,0xe0,0xff,0xc1,0xf0,0x00,0x00,0x00,0x00, 86 | 0x00,0x00,0x00,0x7f,0xe0,0x00,0x3e,0x00,0x7f,0xe0,0xff,0xc1,0xf0,0x00,0x00,0x00,0x00, 87 | 0x00,0x00,0x00,0x7f,0xe0,0x00,0x3e,0x00,0x7f,0xe0,0xff,0xc1,0xf0,0x00,0x00,0x00,0x00, 88 | 0x00,0x00,0x00,0x7f,0xe0,0x00,0x3e,0x00,0x7f,0xe0,0xff,0xc1,0xf0,0x00,0x00,0x00,0x00, 89 | 0x07,0xfe,0x0f,0x80,0x1f,0xff,0xff,0xf0,0x03,0xff,0x07,0xff,0xff,0xfc,0x00,0xf8,0x00, 90 | 0x07,0xfe,0x0f,0x80,0x1f,0xff,0xff,0xf0,0x03,0xff,0x07,0xff,0xff,0xfc,0x00,0xf8,0x00, 91 | 0x07,0xfe,0x0f,0x80,0x1f,0xff,0xff,0xf0,0x03,0xff,0x07,0xff,0xff,0xfc,0x00,0xf8,0x00, 92 | 0x07,0xfe,0x0f,0x80,0x1f,0xff,0xff,0xf0,0x03,0xff,0x07,0xff,0xff,0xfc,0x00,0xf8,0x00, 93 | 0x07,0xfe,0x0f,0x80,0x1f,0xff,0xff,0xf0,0x03,0xff,0x07,0xff,0xff,0xfc,0x00,0xf8,0x00, 94 | 0x00,0x00,0x00,0x00,0x00,0x07,0xfe,0x0f,0x83,0xff,0x07,0xc0,0x00,0x7c,0x1f,0x07,0xc0, 95 | 0x00,0x00,0x00,0x00,0x00,0x07,0xfe,0x0f,0x83,0xff,0x07,0xc0,0x00,0x7c,0x1f,0x07,0xc0, 96 | 0x00,0x00,0x00,0x00,0x00,0x07,0xfe,0x0f,0x83,0xff,0x07,0xc0,0x00,0x7c,0x1f,0x07,0xc0, 97 | 0x00,0x00,0x00,0x00,0x00,0x07,0xfe,0x0f,0x83,0xff,0x07,0xc0,0x00,0x7c,0x1f,0x07,0xc0, 98 | 0x00,0x00,0x00,0x00,0x00,0x07,0xfe,0x0f,0x83,0xff,0x07,0xc0,0x00,0x7c,0x1f,0x07,0xc0, 99 | 0x07,0xff,0xff,0xff,0xff,0x00,0x3e,0x00,0x7f,0xe0,0x07,0xc1,0xf0,0x7f,0xe0,0x07,0xc0, 100 | 0x07,0xff,0xff,0xff,0xff,0x00,0x3e,0x00,0x7f,0xe0,0x07,0xc1,0xf0,0x7f,0xe0,0x07,0xc0, 101 | 0x07,0xff,0xff,0xff,0xff,0x00,0x3e,0x00,0x7f,0xe0,0x07,0xc1,0xf0,0x7f,0xe0,0x07,0xc0, 102 | 0x07,0xff,0xff,0xff,0xff,0x00,0x3e,0x00,0x7f,0xe0,0x07,0xc1,0xf0,0x7f,0xe0,0x07,0xc0, 103 | 0x07,0xff,0xff,0xff,0xff,0x00,0x3e,0x00,0x7f,0xe0,0x07,0xc1,0xf0,0x7f,0xe0,0x07,0xc0, 104 | 0x07,0xc0,0x00,0x00,0x1f,0x00,0x3f,0xff,0x80,0x00,0x07,0xc0,0x00,0x7c,0x00,0x00,0x00, 105 | 0x07,0xc0,0x00,0x00,0x1f,0x00,0x3f,0xff,0x80,0x00,0x07,0xc0,0x00,0x7c,0x00,0x00,0x00, 106 | 0x07,0xc0,0x00,0x00,0x1f,0x00,0x3f,0xff,0x80,0x00,0x07,0xc0,0x00,0x7c,0x00,0x00,0x00, 107 | 0x07,0xc0,0x00,0x00,0x1f,0x00,0x3f,0xff,0x80,0x00,0x07,0xc0,0x00,0x7c,0x00,0x00,0x00, 108 | 0x07,0xc0,0x00,0x00,0x1f,0x00,0x3f,0xff,0x80,0x00,0x07,0xc0,0x00,0x7c,0x00,0x00,0x00, 109 | 0x07,0xc1,0xff,0xfc,0x1f,0x00,0x3f,0xf0,0x03,0xff,0xff,0xff,0xff,0xff,0xe0,0xff,0xc0, 110 | 0x07,0xc1,0xff,0xfc,0x1f,0x00,0x3f,0xf0,0x03,0xff,0xff,0xff,0xff,0xff,0xe0,0xff,0xc0, 111 | 0x07,0xc1,0xff,0xfc,0x1f,0x00,0x3f,0xf0,0x03,0xff,0xff,0xff,0xff,0xff,0xe0,0xff,0xc0, 112 | 0x07,0xc1,0xff,0xfc,0x1f,0x00,0x3f,0xf0,0x03,0xff,0xff,0xff,0xff,0xff,0xe0,0xff,0xc0, 113 | 0x07,0xc1,0xff,0xfc,0x1f,0x00,0x3f,0xf0,0x03,0xff,0xff,0xff,0xff,0xff,0xe0,0xff,0xc0, 114 | 0x07,0xc1,0xff,0xfc,0x1f,0x00,0x00,0x0f,0x80,0x00,0xf8,0x3e,0x00,0x7f,0xff,0xf8,0x00, 115 | 0x07,0xc1,0xff,0xfc,0x1f,0x00,0x00,0x0f,0x80,0x00,0xf8,0x3e,0x00,0x7f,0xff,0xf8,0x00, 116 | 0x07,0xc1,0xff,0xfc,0x1f,0x00,0x00,0x0f,0x80,0x00,0xf8,0x3e,0x00,0x7f,0xff,0xf8,0x00, 117 | 0x07,0xc1,0xff,0xfc,0x1f,0x00,0x00,0x0f,0x80,0x00,0xf8,0x3e,0x00,0x7f,0xff,0xf8,0x00, 118 | 0x07,0xc1,0xff,0xfc,0x1f,0x00,0x00,0x0f,0x80,0x00,0xf8,0x3e,0x00,0x7f,0xff,0xf8,0x00, 119 | 0x07,0xc1,0xff,0xfc,0x1f,0x07,0xc0,0x0f,0xfc,0x1f,0x07,0xc0,0x00,0x7c,0x00,0xff,0xc0, 120 | 0x07,0xc1,0xff,0xfc,0x1f,0x07,0xc0,0x0f,0xfc,0x1f,0x07,0xc0,0x00,0x7c,0x00,0xff,0xc0, 121 | 0x07,0xc1,0xff,0xfc,0x1f,0x07,0xc0,0x0f,0xfc,0x1f,0x07,0xc0,0x00,0x7c,0x00,0xff,0xc0, 122 | 0x07,0xc1,0xff,0xfc,0x1f,0x07,0xc0,0x0f,0xfc,0x1f,0x07,0xc0,0x00,0x7c,0x00,0xff,0xc0, 123 | 0x07,0xc1,0xff,0xfc,0x1f,0x07,0xc0,0x0f,0xfc,0x1f,0x07,0xc0,0x00,0x7c,0x00,0xff,0xc0, 124 | 0x07,0xc0,0x00,0x00,0x1f,0x07,0xff,0xf0,0x7c,0x1f,0xff,0xc0,0x0f,0xfc,0x00,0x00,0x00, 125 | 0x07,0xc0,0x00,0x00,0x1f,0x07,0xff,0xf0,0x7c,0x1f,0xff,0xc0,0x0f,0xfc,0x00,0x00,0x00, 126 | 0x07,0xc0,0x00,0x00,0x1f,0x07,0xff,0xf0,0x7c,0x1f,0xff,0xc0,0x0f,0xfc,0x00,0x00,0x00, 127 | 0x07,0xc0,0x00,0x00,0x1f,0x07,0xff,0xf0,0x7c,0x1f,0xff,0xc0,0x0f,0xfc,0x00,0x00,0x00, 128 | 0x07,0xc0,0x00,0x00,0x1f,0x07,0xff,0xf0,0x7c,0x1f,0xff,0xc0,0x0f,0xfc,0x00,0x00,0x00, 129 | 0x07,0xff,0xff,0xff,0xff,0x00,0x00,0x0f,0xff,0xff,0x07,0xfe,0x00,0x03,0xe0,0x07,0xc0, 130 | 0x07,0xff,0xff,0xff,0xff,0x00,0x00,0x0f,0xff,0xff,0x07,0xfe,0x00,0x03,0xe0,0x07,0xc0, 131 | 0x07,0xff,0xff,0xff,0xff,0x00,0x00,0x0f,0xff,0xff,0x07,0xfe,0x00,0x03,0xe0,0x07,0xc0, 132 | 0x07,0xff,0xff,0xff,0xff,0x00,0x00,0x0f,0xff,0xff,0x07,0xfe,0x00,0x03,0xe0,0x07,0xc0, 133 | 0x07,0xff,0xff,0xff,0xff,0x00,0x00,0x0f,0xff,0xff,0x07,0xfe,0x00,0x03,0xe0,0x07,0xc0, 134 | 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 135 | 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 136 | 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 137 | 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 138 | 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 139 | ] 140 | -------------------------------------------------------------------------------- /gfx/goodbye.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/Python-Thermal-Printer/678e13ae918deadf2b350b3f866a21de6ff1ad6d/gfx/goodbye.png -------------------------------------------------------------------------------- /gfx/hello.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/Python-Thermal-Printer/678e13ae918deadf2b350b3f866a21de6ff1ad6d/gfx/hello.png -------------------------------------------------------------------------------- /gfx/sudoku.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/Python-Thermal-Printer/678e13ae918deadf2b350b3f866a21de6ff1ad6d/gfx/sudoku.png -------------------------------------------------------------------------------- /gfx/timetemp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/Python-Thermal-Printer/678e13ae918deadf2b350b3f866a21de6ff1ad6d/gfx/timetemp.png -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Main script for Adafruit Internet of Things Printer 2. Monitors button 4 | # for taps and holds, performs periodic actions (Twitter polling by default) 5 | # and daily actions (Sudoku and weather by default). 6 | # Written by Adafruit Industries. MIT license. 7 | # 8 | # MUST BE RUN AS ROOT (due to GPIO access) 9 | # 10 | # Required software includes Adafruit_Thermal, Python Imaging and PySerial 11 | # libraries. Other libraries used are part of stock Python install. 12 | # 13 | # Resources: 14 | # http://www.adafruit.com/products/597 Mini Thermal Receipt Printer 15 | # http://www.adafruit.com/products/600 Printer starter pack 16 | 17 | from __future__ import print_function 18 | import RPi.GPIO as GPIO 19 | import subprocess, time, socket 20 | from PIL import Image 21 | from Adafruit_Thermal import * 22 | from datetime import date 23 | import calendar 24 | 25 | ledPin = 18 26 | buttonPin = 23 27 | holdTime = 2 # Duration for button hold (shutdown) 28 | tapTime = 0.01 # Debounce time for button taps 29 | nextInterval = 0.0 # Time of next recurring operation 30 | dailyFlag = False # Set after daily trigger occurs 31 | lastId = '1' # State information passed to/from interval script 32 | printer = Adafruit_Thermal("/dev/serial0", 19200, timeout=5) 33 | 34 | 35 | # Called when button is briefly tapped. Invokes time/temperature script. 36 | def tap(): 37 | GPIO.output(ledPin, GPIO.HIGH) # LED on while working 38 | subprocess.call(["python3", "forecast.py"]) 39 | GPIO.output(ledPin, GPIO.LOW) 40 | 41 | 42 | # Called when button is held down. Prints image, invokes shutdown process. 43 | def hold(): 44 | GPIO.output(ledPin, GPIO.HIGH) 45 | printer.printImage(Image.open('gfx/goodbye.png'), True) 46 | printer.feed(3) 47 | subprocess.call("sync") 48 | subprocess.call(["shutdown", "-h", "now"]) 49 | GPIO.output(ledPin, GPIO.LOW) 50 | 51 | 52 | # Called at periodic intervals (30 seconds by default). 53 | # Invokes twitter script. 54 | def interval(): 55 | GPIO.output(ledPin, GPIO.HIGH) 56 | p = subprocess.Popen(["python3", "twitter.py", str(lastId)], 57 | stdout=subprocess.PIPE) 58 | GPIO.output(ledPin, GPIO.LOW) 59 | return p.communicate()[0] # Script pipes back lastId, returned to main 60 | 61 | 62 | # Called once per day (6:30am by default). 63 | # Invokes weather forecast and sudoku-gfx scripts. 64 | def daily(): 65 | GPIO.output(ledPin, GPIO.HIGH) 66 | subprocess.call(["python3", "forecast.py"]) 67 | d = date.today() 68 | weekday = calendar.day_name[date(d.year,d.month,d.day).weekday()] 69 | if weekday == "Saturday" or weekday == "Sunday": 70 | subprocess.call(["python3", "sudoku-gfx.py"]) 71 | GPIO.output(ledPin, GPIO.LOW) 72 | 73 | 74 | # Initialization 75 | 76 | # Use Broadcom pin numbers (not Raspberry Pi pin numbers) for GPIO 77 | GPIO.setmode(GPIO.BCM) 78 | 79 | # Enable LED and button (w/pull-up on latter) 80 | GPIO.setup(ledPin, GPIO.OUT) 81 | GPIO.setup(buttonPin, GPIO.IN, pull_up_down=GPIO.PUD_UP) 82 | 83 | # LED on while working 84 | GPIO.output(ledPin, GPIO.HIGH) 85 | 86 | # Processor load is heavy at startup; wait a moment to avoid 87 | # stalling during greeting. 88 | time.sleep(30) 89 | 90 | # Show IP address (if network is available) 91 | try: 92 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 93 | s.connect(('8.8.8.8', 0)) 94 | printer.print('My IP address is ' + s.getsockname()[0]) 95 | printer.feed(3) 96 | except: 97 | printer.boldOn() 98 | printer.println('Network is unreachable.') 99 | printer.boldOff() 100 | printer.print('Connect display and keyboard\n' 101 | 'for network troubleshooting.') 102 | printer.feed(3) 103 | exit(0) 104 | 105 | # Print greeting image 106 | printer.printImage(Image.open('gfx/hello.png'), True) 107 | printer.feed(3) 108 | GPIO.output(ledPin, GPIO.LOW) 109 | 110 | # Poll initial button state and time 111 | prevButtonState = GPIO.input(buttonPin) 112 | prevTime = time.time() 113 | tapEnable = False 114 | holdEnable = False 115 | 116 | # Main loop 117 | while(True): 118 | 119 | # Poll current button state and time 120 | buttonState = GPIO.input(buttonPin) 121 | t = time.time() 122 | 123 | # Has button state changed? 124 | if buttonState != prevButtonState: 125 | prevButtonState = buttonState # Yes, save new state/time 126 | prevTime = t 127 | else: # Button state unchanged 128 | if (t - prevTime) >= holdTime: # Button held more than 'holdTime'? 129 | # Yes it has. Is the hold action as-yet untriggered? 130 | if holdEnable == True: # Yep! 131 | hold() # Perform hold action (usu. shutdown) 132 | holdEnable = False # 1 shot...don't repeat hold action 133 | tapEnable = False # Don't do tap action on release 134 | elif (t - prevTime) >= tapTime: # Not holdTime. tapTime elapsed? 135 | # Yes. Debounced press or release... 136 | if buttonState == True: # Button released? 137 | if tapEnable == True: # Ignore if prior hold() 138 | tap() # Tap triggered (button released) 139 | tapEnable = False # Disable tap and hold 140 | holdEnable = False 141 | else: # Button pressed 142 | tapEnable = True # Enable tap and hold actions 143 | holdEnable = True 144 | 145 | # LED blinks while idle, for a brief interval every 2 seconds. 146 | # Pin 18 is PWM-capable and a "sleep throb" would be nice, but 147 | # the PWM-related library is a hassle for average users to install 148 | # right now. Might return to this later when it's more accessible. 149 | if ((int(t) & 1) == 0) and ((t - int(t)) < 0.15): 150 | GPIO.output(ledPin, GPIO.HIGH) 151 | else: 152 | GPIO.output(ledPin, GPIO.LOW) 153 | 154 | # Once per day (currently set for 6:30am local time, or when script 155 | # is first run, if after 6:30am), run forecast and sudoku scripts. 156 | l = time.localtime() 157 | if (60 * l.tm_hour + l.tm_min) > (60 * 6 + 30): 158 | if dailyFlag == False: 159 | daily() 160 | dailyFlag = True 161 | else: 162 | dailyFlag = False # Reset daily trigger 163 | 164 | # Every 30 seconds, run Twitter scripts. 'lastId' is passed around 165 | # to preserve state between invocations. Probably simpler to do an 166 | # import thing. 167 | # if t > nextInterval: 168 | # nextInterval = t + 30.0 169 | # result = interval() 170 | # if result is not None: 171 | # lastId = result.rstrip('\r\n') 172 | 173 | -------------------------------------------------------------------------------- /printertest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from Adafruit_Thermal import * 4 | 5 | printer = Adafruit_Thermal("/dev/serial0", 19200, timeout=5) 6 | 7 | # Test inverse on & off 8 | printer.inverseOn() 9 | printer.println("Inverse ON") 10 | printer.inverseOff() 11 | 12 | # Test character double-height on & off 13 | printer.doubleHeightOn() 14 | printer.println("Double Height ON") 15 | printer.doubleHeightOff() 16 | 17 | # Set justification (right, center, left) -- accepts 'L', 'C', 'R' 18 | printer.justify('R') 19 | printer.println("Right justified") 20 | printer.justify('C') 21 | printer.println("Center justified") 22 | printer.justify('L') 23 | printer.println("Left justified") 24 | 25 | # Test more styles 26 | printer.boldOn() 27 | printer.println("Bold text") 28 | printer.boldOff() 29 | 30 | printer.underlineOn() 31 | printer.println("Underlined text") 32 | printer.underlineOff() 33 | 34 | printer.setSize('L') # Set type size, accepts 'S', 'M', 'L' 35 | printer.println("Large") 36 | printer.setSize('M') 37 | printer.println("Medium") 38 | printer.setSize('S') 39 | printer.println("Small") 40 | 41 | printer.justify('C') 42 | printer.println("normal\nline\nspacing") 43 | printer.setLineHeight(50) 44 | printer.println("Taller\nline\nspacing") 45 | printer.setLineHeight() # Reset to default 46 | printer.justify('L') 47 | 48 | # Barcode examples 49 | printer.feed(1) 50 | # CODE39 is the most common alphanumeric barcode 51 | printer.printBarcode("ADAFRUT", printer.CODE39) 52 | printer.setBarcodeHeight(100) 53 | # Print UPC line on product barcodes 54 | printer.printBarcode("123456789123", printer.UPC_A) 55 | 56 | # Print the 75x75 pixel logo in adalogo.py 57 | import gfx.adalogo as adalogo 58 | printer.printBitmap(adalogo.width, adalogo.height, adalogo.data) 59 | 60 | # Print the 135x135 pixel QR code in adaqrcode.py 61 | import gfx.adaqrcode as adaqrcode 62 | printer.printBitmap(adaqrcode.width, adaqrcode.height, adaqrcode.data) 63 | printer.println("Adafruit!") 64 | printer.feed(2) 65 | 66 | printer.sleep() # Tell printer to sleep 67 | printer.wake() # Call wake() before printing again, even if reset 68 | printer.setDefault() # Restore printer to defaults 69 | -------------------------------------------------------------------------------- /sudoku-gfx.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # Sudoku Generator and Solver in 250 lines of python 4 | # Copyright (c) 2006 David Bau. All rights reserved. 5 | # 6 | # Can be used as either a command-line tool or as a cgi script. 7 | # 8 | # As a cgi-script, generates puzzles and estimates their level of 9 | # difficulty. Uses files sudoku-template.pdf/.ps/.txt/.html 10 | # in which it can fill in 81 underscores with digits for a puzzle. 11 | # The suffix of the request URL determines which template is used. 12 | # 13 | # On a command line without any arguments, prints text for a 14 | # random sudoku puzzle, with an estimate of its difficulty. 15 | # On a command line with a filename, solves the given puzzles 16 | # (files should look like the text generated by the generator). 17 | # 18 | # Adapted for Adafruit_Thermal library by Phil Burgess for Adafruit 19 | # Industries. This version uses bitmaps (in the 'gfx' subdirectory) 20 | # to render the puzzle rather than text symbols. See sudoku-txt 21 | # for a different Sudoku example that's all text-based. 22 | 23 | from __future__ import print_function 24 | import sys, os, random, getopt, re 25 | from Adafruit_Thermal import * 26 | from PIL import Image 27 | 28 | printer = Adafruit_Thermal("/dev/serial0", 19200, timeout=5) 29 | bg = Image.new("1", [384, 426], "white") # Working 'background' image 30 | img = Image.open('gfx/sudoku.png') # Source bitmaps 31 | xcoord = [ 15, 55, 95, 139, 179, 219, 263, 303, 343 ] 32 | ycoord = [ 56, 96, 136, 180, 220, 260, 304, 344, 384 ] 33 | numbers = [] 34 | 35 | def main(): 36 | # Crop number bitmaps out of source image 37 | for i in range(9): 38 | numbers.append(img.crop([384, i*28, 410, (i+1)*28])) 39 | args = sys.argv[1:] 40 | if len(args) > 0: 41 | puzzles = [loadboard(filename) for filename in args] 42 | else: 43 | puzzles = [makepuzzle(solution([None] * 81))] 44 | for puzzle in puzzles: 45 | printboard(puzzle) # Doesn't print, just modifies 'bg' image 46 | printer.printImage(bg, True) # This does the printing 47 | printer.println("RATING:", ratepuzzle(puzzle, 4)) 48 | if len(args) > 0: 49 | printer.println() 50 | printer.println("SOLUTION:") 51 | answer = solution(puzzle) 52 | if answer is None: printer.println("NO SOLUTION") 53 | else: printer.print(printboard(answer)) 54 | printer.feed(3) 55 | 56 | def makepuzzle(board): 57 | puzzle = []; deduced = [None] * 81 58 | order = random.sample(range(81), 81) 59 | for pos in order: 60 | if deduced[pos] is None: 61 | puzzle.append((pos, board[pos])) 62 | deduced[pos] = board[pos] 63 | deduce(deduced) 64 | random.shuffle(puzzle) 65 | for i in range(len(puzzle) - 1, -1, -1): 66 | e = puzzle[i]; del puzzle[i] 67 | rating = checkpuzzle(boardforentries(puzzle), board) 68 | if rating == -1: puzzle.append(e) 69 | return boardforentries(puzzle) 70 | 71 | def ratepuzzle(puzzle, samples): 72 | total = 0 73 | for i in range(samples): 74 | state, answer = solveboard(puzzle) 75 | if answer is None: return -1 76 | total += len(state) 77 | return float(total) / samples 78 | 79 | def checkpuzzle(puzzle, board = None): 80 | state, answer = solveboard(puzzle) 81 | if answer is None: return -1 82 | if board is not None and not boardmatches(board, answer): return -1 83 | difficulty = len(state) 84 | state, second = solvenext(state) 85 | if second is not None: return -1 86 | return difficulty 87 | 88 | def solution(board): 89 | return solveboard(board)[1] 90 | 91 | def solveboard(original): 92 | board = list(original) 93 | guesses = deduce(board) 94 | if guesses is None: return ([], board) 95 | track = [(guesses, 0, board)] 96 | return solvenext(track) 97 | 98 | def solvenext(remembered): 99 | while len(remembered) > 0: 100 | guesses, c, board = remembered.pop() 101 | if c >= len(guesses): continue 102 | remembered.append((guesses, c + 1, board)) 103 | workspace = list(board) 104 | pos, n = guesses[c] 105 | workspace[pos] = n 106 | guesses = deduce(workspace) 107 | if guesses is None: return (remembered, workspace) 108 | remembered.append((guesses, 0, workspace)) 109 | return ([], None) 110 | 111 | def deduce(board): 112 | while True: 113 | stuck, guess, count = True, None, 0 114 | # fill in any spots determined by direct conflicts 115 | allowed, needed = figurebits(board) 116 | for pos in range(81): 117 | if None == board[pos]: 118 | numbers = listbits(allowed[pos]) 119 | if len(numbers) == 0: return [] 120 | elif len(numbers) == 1: board[pos] = numbers[0]; stuck = False 121 | elif stuck: 122 | guess, count = pickbetter(guess, count, [(pos, n) for n in numbers]) 123 | if not stuck: allowed, needed = figurebits(board) 124 | # fill in any spots determined by elimination of other locations 125 | for axis in range(3): 126 | for x in range(9): 127 | numbers = listbits(needed[axis * 9 + x]) 128 | for n in numbers: 129 | bit = 1 << n 130 | spots = [] 131 | for y in range(9): 132 | pos = posfor(x, y, axis) 133 | if allowed[pos] & bit: spots.append(pos) 134 | if len(spots) == 0: return [] 135 | elif len(spots) == 1: board[spots[0]] = n; stuck = False 136 | elif stuck: 137 | guess, count = pickbetter(guess, count, [(pos, n) for pos in spots]) 138 | if stuck: 139 | if guess is not None: random.shuffle(guess) 140 | return guess 141 | 142 | def figurebits(board): 143 | allowed, needed = [e is None and 511 or 0 for e in board], [] 144 | for axis in range(3): 145 | for x in range(9): 146 | bits = axismissing(board, x, axis) 147 | needed.append(bits) 148 | for y in range(9): 149 | allowed[posfor(x, y, axis)] &= bits 150 | return allowed, needed 151 | 152 | def posfor(x, y, axis = 0): 153 | if axis == 0: return x * 9 + y 154 | elif axis == 1: return y * 9 + x 155 | else: return ((0,3,6,27,30,33,54,57,60)[x] + (0,1,2,9,10,11,18,19,20)[y]) 156 | 157 | def axisfor(pos, axis): 158 | if axis == 0: return pos / 9 159 | elif axis == 1: return pos % 9 160 | else: return (pos / 27) * 3 + (pos / 3) % 3 161 | 162 | def axismissing(board, x, axis): 163 | bits = 0 164 | for y in range(9): 165 | e = board[posfor(x, y, axis)] 166 | if e is not None: bits |= 1 << e 167 | return 511 ^ bits 168 | 169 | def listbits(bits): 170 | return [y for y in range(9) if 0 != bits & 1 << y] 171 | 172 | def allowed(board, pos): 173 | bits = 511 174 | for axis in range(3): 175 | x = axisfor(pos, axis) 176 | bits &= axismissing(board, x, axis) 177 | return bits 178 | 179 | def pickbetter(b, c, t): 180 | if b is None or len(t) < len(b): return (t, 1) 181 | if len(t) > len(b): return (b, c) 182 | if random.randint(0, c) == 0: return (t, c + 1) 183 | else: return (b, c + 1) 184 | 185 | def entriesforboard(board): 186 | return [(pos, board[pos]) for pos in range(81) if board[pos] is not None] 187 | 188 | def boardforentries(entries): 189 | board = [None] * 81 190 | for pos, n in entries: board[pos] = n 191 | return board 192 | 193 | def boardmatches(b1, b2): 194 | for i in range(81): 195 | if b1[i] != b2[i]: return False 196 | return True 197 | 198 | def printboard(board): 199 | bg.paste(img, (0, 0)) # Numbers are cropped off right side 200 | for row in range(9): 201 | for col in range(9): 202 | n = board[posfor(row, col)] 203 | if n is not None: 204 | bg.paste(numbers[n], (xcoord[col], ycoord[row])) 205 | 206 | def parseboard(str): 207 | result = [] 208 | for w in str.split(): 209 | for x in w: 210 | if x in '|-=+': continue 211 | if x in '123456789': result.append(int(x) - 1) 212 | else: result.append(None) 213 | if len(result) == 81: return result 214 | 215 | def loadboard(filename): 216 | f = file(filename, 'r') 217 | result = parseboard(f.read()) 218 | f.close() 219 | return result 220 | 221 | def basedir(): 222 | if hasattr(sys.modules[__name__], '__file__'): 223 | return os.path.split(__file__)[0] 224 | elif __name__ == '__main__': 225 | if len(sys.argv) > 0 and sys.argv[0] != '': 226 | return os.path.split(sys.argv[0])[0] 227 | else: 228 | return os.curdir 229 | 230 | def loadsudokutemplate(ext): 231 | f = open(os.path.join(basedir(), 'sudoku-template.%s' % ext), 'r') 232 | result = f.read() 233 | f.close() 234 | return result 235 | 236 | if __name__ == '__main__': 237 | main() 238 | 239 | -------------------------------------------------------------------------------- /sudoku-txt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Sudoku Generator and Solver in 250 lines of python 4 | # Copyright (c) 2006 David Bau. All rights reserved. 5 | # 6 | # Can be used as either a command-line tool or as a cgi script. 7 | # 8 | # As a cgi-script, generates puzzles and estimates their level of 9 | # difficulty. Uses files sudoku-template.pdf/.ps/.txt/.html 10 | # in which it can fill in 81 underscores with digits for a puzzle. 11 | # The suffix of the request URL determines which template is used. 12 | # 13 | # On a command line without any arguments, prints text for a 14 | # random sudoku puzzle, with an estimate of its difficulty. 15 | # On a command line with a filename, solves the given puzzles 16 | # (files should look like the text generated by the generator). 17 | # 18 | # Adapted for Adafruit_Thermal library by Phil Burgess for 19 | # Adafruit Industries. 20 | 21 | from __future__ import print_function 22 | import sys, os, random, getopt, re 23 | from Adafruit_Thermal import * 24 | 25 | printer = Adafruit_Thermal("/dev/serial0", 19200, timeout=5) 26 | 27 | def main(): 28 | printer.setLineHeight(24) # So graphical chars fit together 29 | args = sys.argv[1:] 30 | if len(args) > 0: 31 | puzzles = [loadboard(filename) for filename in args] 32 | else: 33 | puzzles = [makepuzzle(solution([None] * 81))] 34 | for puzzle in puzzles: 35 | printer.println("PUZZLE:") 36 | printer.feed(1) 37 | printer.print(printboard(puzzle)) 38 | printer.feed(1) 39 | printer.println("RATING:", ratepuzzle(puzzle, 4)) 40 | if len(args) > 0: 41 | printer.println() 42 | printer.println("SOLUTION:") 43 | answer = solution(puzzle) 44 | if answer is None: printer.println("NO SOLUTION") 45 | else: printer.print(printboard(answer)) 46 | printer.feed(3) 47 | 48 | def makepuzzle(board): 49 | puzzle = []; deduced = [None] * 81 50 | order = random.sample(xrange(81), 81) 51 | for pos in order: 52 | if deduced[pos] is None: 53 | puzzle.append((pos, board[pos])) 54 | deduced[pos] = board[pos] 55 | deduce(deduced) 56 | random.shuffle(puzzle) 57 | for i in xrange(len(puzzle) - 1, -1, -1): 58 | e = puzzle[i]; del puzzle[i] 59 | rating = checkpuzzle(boardforentries(puzzle), board) 60 | if rating == -1: puzzle.append(e) 61 | return boardforentries(puzzle) 62 | 63 | def ratepuzzle(puzzle, samples): 64 | total = 0 65 | for i in xrange(samples): 66 | state, answer = solveboard(puzzle) 67 | if answer is None: return -1 68 | total += len(state) 69 | return float(total) / samples 70 | 71 | def checkpuzzle(puzzle, board = None): 72 | state, answer = solveboard(puzzle) 73 | if answer is None: return -1 74 | if board is not None and not boardmatches(board, answer): return -1 75 | difficulty = len(state) 76 | state, second = solvenext(state) 77 | if second is not None: return -1 78 | return difficulty 79 | 80 | def solution(board): 81 | return solveboard(board)[1] 82 | 83 | def solveboard(original): 84 | board = list(original) 85 | guesses = deduce(board) 86 | if guesses is None: return ([], board) 87 | track = [(guesses, 0, board)] 88 | return solvenext(track) 89 | 90 | def solvenext(remembered): 91 | while len(remembered) > 0: 92 | guesses, c, board = remembered.pop() 93 | if c >= len(guesses): continue 94 | remembered.append((guesses, c + 1, board)) 95 | workspace = list(board) 96 | pos, n = guesses[c] 97 | workspace[pos] = n 98 | guesses = deduce(workspace) 99 | if guesses is None: return (remembered, workspace) 100 | remembered.append((guesses, 0, workspace)) 101 | return ([], None) 102 | 103 | def deduce(board): 104 | while True: 105 | stuck, guess, count = True, None, 0 106 | # fill in any spots determined by direct conflicts 107 | allowed, needed = figurebits(board) 108 | for pos in xrange(81): 109 | if None == board[pos]: 110 | numbers = listbits(allowed[pos]) 111 | if len(numbers) == 0: return [] 112 | elif len(numbers) == 1: board[pos] = numbers[0]; stuck = False 113 | elif stuck: 114 | guess, count = pickbetter(guess, count, [(pos, n) for n in numbers]) 115 | if not stuck: allowed, needed = figurebits(board) 116 | # fill in any spots determined by elimination of other locations 117 | for axis in xrange(3): 118 | for x in xrange(9): 119 | numbers = listbits(needed[axis * 9 + x]) 120 | for n in numbers: 121 | bit = 1 << n 122 | spots = [] 123 | for y in xrange(9): 124 | pos = posfor(x, y, axis) 125 | if allowed[pos] & bit: spots.append(pos) 126 | if len(spots) == 0: return [] 127 | elif len(spots) == 1: board[spots[0]] = n; stuck = False 128 | elif stuck: 129 | guess, count = pickbetter(guess, count, [(pos, n) for pos in spots]) 130 | if stuck: 131 | if guess is not None: random.shuffle(guess) 132 | return guess 133 | 134 | def figurebits(board): 135 | allowed, needed = [e is None and 511 or 0 for e in board], [] 136 | for axis in xrange(3): 137 | for x in xrange(9): 138 | bits = axismissing(board, x, axis) 139 | needed.append(bits) 140 | for y in xrange(9): 141 | allowed[posfor(x, y, axis)] &= bits 142 | return allowed, needed 143 | 144 | def posfor(x, y, axis = 0): 145 | if axis == 0: return x * 9 + y 146 | elif axis == 1: return y * 9 + x 147 | else: return ((0,3,6,27,30,33,54,57,60)[x] + (0,1,2,9,10,11,18,19,20)[y]) 148 | 149 | def axisfor(pos, axis): 150 | if axis == 0: return pos / 9 151 | elif axis == 1: return pos % 9 152 | else: return (pos / 27) * 3 + (pos / 3) % 3 153 | 154 | def axismissing(board, x, axis): 155 | bits = 0 156 | for y in xrange(9): 157 | e = board[posfor(x, y, axis)] 158 | if e is not None: bits |= 1 << e 159 | return 511 ^ bits 160 | 161 | def listbits(bits): 162 | return [y for y in xrange(9) if 0 != bits & 1 << y] 163 | 164 | def allowed(board, pos): 165 | bits = 511 166 | for axis in xrange(3): 167 | x = axisfor(pos, axis) 168 | bits &= axismissing(board, x, axis) 169 | return bits 170 | 171 | def pickbetter(b, c, t): 172 | if b is None or len(t) < len(b): return (t, 1) 173 | if len(t) > len(b): return (b, c) 174 | if random.randint(0, c) == 0: return (t, c + 1) 175 | else: return (b, c + 1) 176 | 177 | def entriesforboard(board): 178 | return [(pos, board[pos]) for pos in xrange(81) if board[pos] is not None] 179 | 180 | def boardforentries(entries): 181 | board = [None] * 81 182 | for pos, n in entries: board[pos] = n 183 | return board 184 | 185 | def boardmatches(b1, b2): 186 | for i in xrange(81): 187 | if b1[i] != b2[i]: return False 188 | return True 189 | 190 | def printboard(board): 191 | # Top edge of board: 192 | out = (' ' 193 | + chr(0xC9) # Top left corner 194 | + chr(0xCD) # Top edge 195 | + chr(0xD1) # Top 'T' 196 | + chr(0xCD) # Top edge 197 | + chr(0xD1) # Top 'T' 198 | + chr(0xCD) # Top edge 199 | + chr(0xCB) # Top 'T' (double) 200 | + chr(0xCD) # Top edge 201 | + chr(0xD1) # Top 'T' 202 | + chr(0xCD) # Top edge 203 | + chr(0xD1) # Top 'T' 204 | + chr(0xCD) # Top edge 205 | + chr(0xCB) # Top 'T' (double) 206 | + chr(0xCD) # Top edge 207 | + chr(0xD1) # Top 'T' 208 | + chr(0xCD) # Top edge 209 | + chr(0xD1) # Top 'T' 210 | + chr(0xCD) # Top edge 211 | + chr(0xBB) # Top right corner 212 | + '\n') 213 | for row in xrange(9): 214 | out += (' ' + chr(0xBA)) # Double Bar 215 | for col in xrange(9): 216 | n = board[posfor(row, col)] 217 | if n is None: 218 | out += ' ' 219 | else: 220 | out += str(n+1) 221 | if (col == 2) or (col == 5) or (col == 8): 222 | out += chr(0xBA) # Double bar 223 | else: 224 | out += chr(0xB3) # Single bar 225 | out += '\n' 226 | if(row < 8): 227 | if(row == 2) or (row == 5): 228 | out += (' ' 229 | + chr(0xCC) # Left 'T' (double) 230 | + chr(0xCD) # Horizontal bar 231 | + chr(0xD8) # + 232 | + chr(0xCD) # Horizontal bar 233 | + chr(0xD8) # + 234 | + chr(0xCD) # Horizontal bar 235 | + chr(0xCE) # Double + 236 | + chr(0xCD) # Horizontal bar 237 | + chr(0xD8) # + 238 | + chr(0xCD) # Horizontal bar 239 | + chr(0xD8) # + 240 | + chr(0xCD) # Horizontal bar 241 | + chr(0xCE) # Double + 242 | + chr(0xCD) # Horizontal bar 243 | + chr(0xD8) # + 244 | + chr(0xCD) # Horizontal bar 245 | + chr(0xD8) # + 246 | + chr(0xCD) # Horizontal bar 247 | + chr(0xB9) # Right 'T' (double) 248 | + '\n') 249 | else: 250 | out += (' ' 251 | + chr(0xC7) # Left 'T' 252 | + chr(0xC4) # Horizontal bar 253 | + chr(0xC5) # + 254 | + chr(0xC4) # Horizontal bar 255 | + chr(0xC5) # + 256 | + chr(0xC4) # Horizontal bar 257 | + chr(0xD7) # Double + 258 | + chr(0xC4) # Horizontal bar 259 | + chr(0xC5) # + 260 | + chr(0xC4) # Horizontal bar 261 | + chr(0xC5) # + 262 | + chr(0xC4) # Horizontal bar 263 | + chr(0xD7) # Double + 264 | + chr(0xC4) # Horizontal bar 265 | + chr(0xC5) # + 266 | + chr(0xC4) # Horizontal bar 267 | + chr(0xC5) # + 268 | + chr(0xC4) # Horizontal bar 269 | + chr(0xB6) # Right 'T' 270 | + '\n') 271 | out += (' ' 272 | + chr(0xC8) # Bottom left corner 273 | + chr(0xCD) # Bottom edge 274 | + chr(0xCF) # Bottom 'T' 275 | + chr(0xCD) # Bottom edge 276 | + chr(0xCF) # Bottom 'T' 277 | + chr(0xCD) # Bottom edge 278 | + chr(0xCA) # Bottom 'T' (double) 279 | + chr(0xCD) # Bottom edge 280 | + chr(0xCF) # Bottom 'T' 281 | + chr(0xCD) # Bottom edge 282 | + chr(0xCF) # Bottom 'T' 283 | + chr(0xCD) # Bottom edge 284 | + chr(0xCA) # Bottom 'T' (double) 285 | + chr(0xCD) # Bottom edge 286 | + chr(0xCF) # Bottom 'T' 287 | + chr(0xCD) # Bottom edge 288 | + chr(0xCF) # Bottom 'T' 289 | + chr(0xCD) # Bottom edge 290 | + chr(0xBC) # Bottom-right corner 291 | + '\n') 292 | 293 | # Original output code: 294 | # out = "" 295 | # for row in xrange(9): 296 | # for col in xrange(9): 297 | # out += (""," "," "," "," "," "," "," "," ")[col] 298 | # out += printcode(board[posfor(row, col)]) 299 | # out += ('\n','\n','\n\n','\n','\n','\n\n','\n','\n','\n')[row] 300 | return out 301 | 302 | def parseboard(str): 303 | result = [] 304 | for w in str.split(): 305 | for x in w: 306 | if x in '|-=+': continue 307 | if x in '123456789': result.append(int(x) - 1) 308 | else: result.append(None) 309 | if len(result) == 81: return result 310 | 311 | def loadboard(filename): 312 | f = file(filename, 'r') 313 | result = parseboard(f.read()) 314 | f.close() 315 | return result 316 | 317 | def basedir(): 318 | if hasattr(sys.modules[__name__], '__file__'): 319 | return os.path.split(__file__)[0] 320 | elif __name__ == '__main__': 321 | if len(sys.argv) > 0 and sys.argv[0] != '': 322 | return os.path.split(sys.argv[0])[0] 323 | else: 324 | return os.curdir 325 | 326 | def loadsudokutemplate(ext): 327 | f = open(os.path.join(basedir(), 'sudoku-template.%s' % ext), 'r') 328 | result = f.read() 329 | f.close() 330 | return result 331 | 332 | if __name__ == '__main__': 333 | main() 334 | -------------------------------------------------------------------------------- /timetemp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Current time and temperature display for Raspberry Pi w/Adafruit Mini 4 | # Thermal Printer. Retrieves data from DarkSky.net's API, prints current 5 | # conditions and time using large, friendly graphics. 6 | # See forecast.py for a different weather example that's all text-based. 7 | # Written by Adafruit Industries. MIT license. 8 | # 9 | # Required software includes Adafruit_Thermal, Python Imaging and PySerial 10 | # libraries. Other libraries used are part of stock Python install. 11 | # 12 | # Resources: 13 | # http://www.adafruit.com/products/597 Mini Thermal Receipt Printer 14 | # http://www.adafruit.com/products/600 Printer starter pack 15 | 16 | from __future__ import print_function 17 | from Adafruit_Thermal import * 18 | import time, urllib.request, json 19 | from PIL import Image, ImageDraw 20 | 21 | API_KEY = "YOUR_OPEN_WEATHER_API_KEY" 22 | 23 | cityName = "YOUR_CITY_NAME" 24 | 25 | # Fetch weather data from DarkSky, parse resulting JSON 26 | url = f"http://api.openweathermap.org/data/2.5/weather?q={cityName}&appid={API_KEY}" 27 | response = urllib.request.urlopen(url) 28 | data = json.loads(response.read()) 29 | print(data) 30 | # Extract values relating to current temperature, humidity, wind 31 | 32 | temperature = (int(data['main']['temp']) - 273.15) * 9/5 + 32 33 | humidity = int(data['main']['humidity'] * 100); 34 | windSpeed = int(data['wind']['speed']) 35 | windDir = data['wind']['deg'] 36 | windUnits = "mph" 37 | 38 | # print(temperature) 39 | # print(humidity) 40 | # print(windSpeed) 41 | # print(windDir) 42 | # print(windUnits) 43 | 44 | # Although the Python Imaging Library does have nice font support, 45 | # I opted here to use a raster bitmap for all of the glyphs instead. 46 | # This allowed lots of control over kerning and such, and I didn't 47 | # want to spend a lot of time hunting down a suitable font with a 48 | # permissive license. 49 | symbols = Image.open("gfx/timetemp.png") # Bitmap w/all chars & symbols 50 | img = Image.new("1", [330, 117], "white") # Working 'background' image 51 | draw = ImageDraw.Draw(img) 52 | 53 | # These are the widths of certain glyphs within the 'symbols' bitmap 54 | TimeDigitWidth = [ 38, 29, 38, 36, 40, 35, 37, 37, 38, 37, 13 ] 55 | TempDigitWidth = [ 33, 25, 32, 31, 35, 30, 32, 32, 33, 32, 17, 14 ] 56 | DateDigitWidth = [ 16, 13, 16, 15, 17, 15, 16, 16, 16, 16 ] 57 | HumiDigitWidth = [ 14, 10, 14, 13, 15, 12, 13, 13, 13, 13, 18 ] 58 | DayWidth = [ 104, 109, 62, 110, 88, 110, 95 ] 59 | MonthWidth = [ 53, 52, 60, 67, 59, 63, 59, 56, 51, 48, 54, 53 ] 60 | DirWidth = [ 23, 35, 12, 27, 15, 33, 19, 41, 23 ] 61 | DirAngle = [ 23, 68, 113, 157, 203, 247, 293, 336 ] 62 | 63 | # Generate a list of sub-image glyphs cropped from the symbols image 64 | def croplist(widths, x, y, height): 65 | list = [] 66 | for i in range(len(widths)): 67 | list.append(symbols.crop( 68 | [x, y+i*height, x+widths[i], y+(i+1)*height])) 69 | return list 70 | 71 | # Crop glyph lists (digits, days of week, etc.) 72 | TimeDigit = croplist(TimeDigitWidth, 0, 0, 44) 73 | TempDigit = croplist(TempDigitWidth, 40, 0, 39) 74 | DateDigit = croplist(DateDigitWidth, 75, 0, 18) 75 | HumiDigit = croplist(HumiDigitWidth, 75, 180, 16) 76 | Day = croplist(DayWidth , 93, 0, 25) 77 | Month = croplist(MonthWidth , 93, 175, 24) 78 | Dir = croplist(DirWidth , 162, 175, 21) 79 | # Crop a few odds-and-ends glyphs (not in lists) 80 | Wind = symbols.crop([ 93, 463, 157, 479 ]) 81 | Humidity = symbols.crop([ 93, 479, 201, 500 ]) 82 | Kph = symbols.crop([ 156, 366, 196, 386 ]) 83 | Mph = symbols.crop([ 156, 387, 203, 407 ]) 84 | 85 | # Draw top & bottom bars 86 | draw.rectangle([42, 0, 330, 3], fill="black") 87 | draw.rectangle([42, 113, 330, 116], fill="black") 88 | 89 | x = 42 # Initial drawing position 90 | y = 12 91 | 92 | # Paste a series of glyphs (mostly numbers) from string to img 93 | def drawNums(str, x, y, list): 94 | for i in range(len(str)): 95 | d = ord(str[i]) - ord('0') 96 | img.paste(list[d], (x, y)) 97 | x += list[d].size[0] + 1 98 | return x 99 | 100 | # Determine total width of a series of glyphs in string 101 | def numWidth(str, list): 102 | w = 0 # Cumulative width 103 | for i in range(len(str)): 104 | d = ord(str[i]) - ord('0') 105 | if i > 0: w += 1 # Space between digits 106 | w += list[d].size[0] # Digit width 107 | return w 108 | 109 | # Render current time (always 24 hour XX:XX format) 110 | t = time.localtime() 111 | drawNums(time.strftime("%H:%M", t), x, y, TimeDigit) 112 | 113 | # Determine wider of day-of-week or date (for alignment) 114 | s = str(t.tm_mday) # Convert day of month to a string 115 | w = MonthWidth[t.tm_mon - 1] + 6 + numWidth(s, DateDigit) 116 | if DayWidth[t.tm_wday] > w: w = DayWidth[t.tm_wday] 117 | 118 | # Draw day-of-week and date 119 | x = img.size[0] - w # Left alignment for two lines 120 | img.paste(Day[t.tm_wday], (x, y)) # Draw day of week word 121 | y += 27 # Next line 122 | img.paste(Month[t.tm_mon - 1], (x, y)) # Draw month word 123 | x += MonthWidth[t.tm_mon - 1] + 6 # Advance past month 124 | drawNums(s, x, y, DateDigit) # Draw day of month 125 | 126 | x = 42 # Position for temperature 127 | y = 67 128 | # Degrees to string, remap '-' glyph, append degrees glyph 129 | s = str(temperature).replace('-', ';') + ':' 130 | drawNums(s, x, y, TempDigit) 131 | 132 | # Determine wider of humidity or wind info 133 | s = str(humidity) + ':' # Appends percent glyph 134 | s2 = str(windSpeed) 135 | winDirNum = 0 # Wind direction glyph number 136 | if windSpeed > 0: 137 | for winDirNum in range(len(DirAngle) - 1): 138 | if windDir < DirAngle[winDirNum]: break 139 | winDirNum+=1 140 | w = Humidity.size[0] + 5 + numWidth(s, HumiDigit) 141 | w2 = Wind.size[0] + 5 + numWidth(s2, HumiDigit) 142 | if windSpeed > 0: 143 | w2 += 3 + Dir[winDirNum].size[0] 144 | if windUnits == 'kph': w2 += 3 + Kph.size[0] 145 | else: w2 += 3 + Mph.size[0] 146 | if w2 > w: w = w2 147 | 148 | # Draw humidity and wind 149 | x = img.size[0] - w # Left-align the two lines 150 | y = 67 151 | img.paste(Humidity, (x, y)) 152 | x += Humidity.size[0] + 5 153 | drawNums(s, x, y, HumiDigit) 154 | x = img.size[0] - w # Left-align again 155 | y += 23 # And advance to next line 156 | img.paste(Wind, (x, y)) 157 | x += Wind.size[0] + 5 158 | 159 | if windSpeed > 0: 160 | img.paste(Dir[winDirNum], (x, y)) 161 | x += Dir[winDirNum].size[0] + 3 162 | x = drawNums(s2, x, y, HumiDigit) + 3 163 | if windUnits == 'kph': img.paste(Kph, (x, y)) 164 | else: img.paste(Mph, (x, y)) 165 | 166 | # Open connection to printer and print image 167 | printer = Adafruit_Thermal("/dev/serial0", 19200, timeout=5) 168 | printer.printImage(img, True) 169 | printer.feed(3) 170 | -------------------------------------------------------------------------------- /twitter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # This is a Python port of Adafruit's "Gutenbird" sketch for Arduino. 4 | # Polls one or more Twitter accounts for changes, displaying updates 5 | # on attached thermal printer. 6 | # Written by Adafruit Industries. MIT license. 7 | # 8 | # Required hardware includes an Internet-connected system with Python 9 | # (such as Raspberry Pi) and an Adafruit Mini Thermal Receipt printer 10 | # and all related power supplies and cabling. 11 | # Required software includes Adafruit_Thermal and PySerial libraries. 12 | # 13 | # Resources: 14 | # http://www.adafruit.com/products/597 Mini Thermal Receipt Printer 15 | # http://www.adafruit.com/products/600 Printer starter pack 16 | # 17 | # Uses Twitter 1.1 API application-only authentication. This 18 | # REQUIRES a Twitter account and some account configuration. Start 19 | # at dev.twitter.com, sign in with your Twitter credentials, select 20 | # "My Applications" from the avatar drop-down menu at the top right, 21 | # then "Create a new application." Provide a name, description, 22 | # placeholder URL and complete the captcha, after which you'll be 23 | # provided a "consumer key" and "consumer secret" for your app. 24 | # Copy these strings to the globals below, and configure the search 25 | # string to your liking. DO NOT SHARE your consumer key or secret! 26 | # If you put code on Github or other public repository, replace them 27 | # with dummy strings. 28 | 29 | from __future__ import print_function 30 | import base64, HTMLParser, httplib, json, sys, urllib, zlib 31 | from unidecode import unidecode 32 | from Adafruit_Thermal import * 33 | 34 | 35 | # Configurable globals. Edit to your needs. ------------------------------- 36 | 37 | # Twitter application credentials -- see notes above -- DO NOT SHARE. 38 | consumer_key = 'PUT_YOUR_CONSUMER_KEY_HERE' 39 | consumer_secret = 'PUT_YOUR_CONSUMER_SECRET_HERE' 40 | 41 | # queryString can be any valid Twitter API search string, including 42 | # boolean operators. See https://developer.twitter.com/en/docs/tweets/search/api-reference/get-search-tweets 43 | # for options and syntax. Funny characters do NOT need to be URL 44 | # encoded here -- urllib takes care of that. 45 | queryString = 'from:Adafruit' 46 | 47 | 48 | # Other globals. You probably won't need to change these. ----------------- 49 | 50 | printer = Adafruit_Thermal("/dev/serial0", 19200, timeout=5) 51 | host = 'api.twitter.com' 52 | authUrl = '/oauth2/token' 53 | searchUrl = '/1.1/search/tweets.json?' 54 | agent = 'Gutenbird v1.0' 55 | # lastID is command line value (if passed), else 1 56 | if len(sys.argv) > 1: lastId = sys.argv[1] 57 | else: lastId = '1' 58 | 59 | # Initiate an HTTPS connection/request, uncompress and JSON-decode results 60 | def issueRequestAndDecodeResponse(method, url, body, headers): 61 | connection = httplib.HTTPSConnection(host) 62 | connection.request(method, url, body, headers) 63 | response = connection.getresponse() 64 | if response.status != 200: 65 | # This is OK for command-line testing, otherwise 66 | # keep it commented out when using main.py 67 | # print('HTTP error: %d' % response.status) 68 | exit(-1) 69 | compressed = response.read() 70 | connection.close() 71 | return json.loads(zlib.decompress(compressed, 16+zlib.MAX_WBITS)) 72 | 73 | 74 | # Mainline code. ----------------------------------------------------------- 75 | 76 | # Get access token. -------------------------------------------------------- 77 | 78 | token = issueRequestAndDecodeResponse( 79 | 'POST', authUrl, 'grant_type=client_credentials', 80 | {'Host' : host, 81 | 'User-Agent' : agent, 82 | 'Accept-Encoding' : 'gzip', 83 | 'Content-Type' : 'application/x-www-form-urlencoded;charset=UTF-8', 84 | 'Authorization' : 'Basic ' + base64.b64encode( 85 | urllib.quote(consumer_key) + ':' + urllib.quote(consumer_secret))} 86 | )['access_token'] 87 | 88 | 89 | # Perform search. ---------------------------------------------------------- 90 | 91 | data = issueRequestAndDecodeResponse( 92 | 'GET', 93 | (searchUrl + 'count=3&since_id=%s&q=%s' % 94 | (lastId, urllib.quote(queryString))), 95 | None, 96 | {'Host' : host, 97 | 'User-Agent' : agent, 98 | 'Accept-Encoding' : 'gzip', 99 | 'Authorization' : 'Bearer ' + token}) 100 | 101 | 102 | # Display results. --------------------------------------------------------- 103 | 104 | maxId = data['search_metadata']['max_id_str'] 105 | 106 | for tweet in data['statuses']: 107 | 108 | printer.inverseOn() 109 | printer.print(' ' + '{:<31}'.format(tweet['user']['screen_name'])) 110 | printer.inverseOff() 111 | 112 | printer.underlineOn() 113 | printer.print('{:<32}'.format(tweet['created_at'])) 114 | printer.underlineOff() 115 | 116 | # max_id_str is not always present, so check tweet IDs as fallback 117 | id = tweet['id_str'] 118 | if(id > maxId): maxId = id # String compare is OK for this 119 | 120 | # Remove HTML escape sequences 121 | # and remap Unicode values to nearest ASCII equivalents 122 | printer.print(unidecode( 123 | HTMLParser.HTMLParser().unescape(tweet['text']))) 124 | 125 | printer.feed(3) 126 | 127 | print(maxId) # Piped back to calling process 128 | --------------------------------------------------------------------------------