/ adafruit_thermal_printer / thermal_printer.py
thermal_printer.py
1 # The MIT License (MIT) 2 # 3 # Copyright (c) 2017 Tony DiCola 4 # 5 # Permission is hereby granted, free of charge, to any person obtaining a copy 6 # of this software and associated documentation files (the "Software"), to deal 7 # in the Software without restriction, including without limitation the rights 8 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 # copies of the Software, and to permit persons to whom the Software is 10 # furnished to do so, subject to the following conditions: 11 # 12 # The above copyright notice and this permission notice shall be included in 13 # all copies or substantial portions of the Software. 14 # 15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 # THE SOFTWARE. 22 """ 23 `adafruit_thermal_printer.thermal_printer` - Thermal Printer Driver 24 ===================================================================== 25 26 Thermal printer control module built to work with small serial thermal 27 receipt printers. Note that these printers have many different firmware 28 versions and care must be taken to select the appropriate module inside this 29 package for your firmware printer: 30 31 * thermal_printer = The latest printers with firmware version 2.68+ 32 * thermal_printer_264 = Printers with firmware version 2.64 up to 2.68. 33 * thermal_printer_legacy = Printers with firmware version before 2.64. 34 35 * Author(s): Tony DiCola 36 37 Implementation Notes 38 -------------------- 39 40 **Hardware:** 41 42 * Mini `Thermal Receipt Printer 43 <https://www.adafruit.com/product/597>`_ (Product ID: 597) 44 45 **Software and Dependencies:** 46 47 * Adafruit CircuitPython firmware for the ESP8622 and M0-based boards: 48 https://github.com/adafruit/circuitpython/releases 49 50 """ 51 import time 52 53 from micropython import const 54 55 56 __version__ = "0.0.0-auto.0" 57 __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Thermal_Printer.git" 58 59 60 # pylint: disable=bad-whitespace 61 # Internally used constants. 62 _UPDOWN_MASK = const(1 << 2) 63 _BOLD_MASK = const(1 << 3) 64 _DOUBLE_HEIGHT_MASK = const(1 << 4) 65 _DOUBLE_WIDTH_MASK = const(1 << 5) 66 _STRIKE_MASK = const(1 << 6) 67 68 # External constants: 69 JUSTIFY_LEFT = const(0) 70 JUSTIFY_CENTER = const(1) 71 JUSTIFY_RIGHT = const(2) 72 SIZE_SMALL = const(0) 73 SIZE_MEDIUM = const(1) 74 SIZE_LARGE = const(2) 75 UNDERLINE_THIN = const(0) 76 UNDERLINE_THICK = const(1) 77 # pylint: enable=bad-whitespace 78 79 80 # Disable too many instance members warning. This is not something pylint can 81 # reasonably infer--the complexity of instance variables is required for proper 82 # printer function. Disable this warning. 83 # pylint: disable=too-many-instance-attributes 84 85 # Disable too many public members warning. Again this is not something pylint 86 # can reasonably decide. Thermal printers require lots of control functions. 87 # Disable this warning. 88 # pylint: disable=too-many-public-methods 89 90 # Thermal printer class for printers with firmware version 2.68 and higher. 91 # Do not modify this class without fully understanding its coupling to the 92 # legacy and 2.64+ version printer which inherit from it. These legacy printer 93 # classes override specific functions which have different requirements of 94 # behavior between different versions of printer firmware. Firmware printers 95 # vary _greatly_ in their command set--there is not a clean abstraction. The 96 # assumption here is that this class is the master with logic for the most 97 # recent (2.68+) firmware printers. Older firmware versions inherit and 98 # override behavior where necessary. It is highly, HIGHLY recommended you 99 # carefully study the Arduino thermal printer library code and fully 100 # understand all the firmware differences (notice where the library changes 101 # behavior with the firmware version define): 102 # https://github.com/adafruit/Adafruit-Thermal-Printer-Library 103 # Bottom line: don't touch this code without understanding the big picture or 104 # else it will be very easy to break or introduce subtle incompatibilities with 105 # older firmware printers. 106 class ThermalPrinter: 107 """Thermal printer for printers with firmware version 2.68 or higher.""" 108 109 # pylint: disable=bad-whitespace 110 # Barcode types. These vary based on the firmware version so are made 111 # as class-level variables that users can reference (i.e. 112 # ThermalPrinter.UPC_A, etc) and write code that is independent of the 113 # printer firmware version. 114 UPC_A = 65 115 UPC_E = 66 116 EAN13 = 67 117 EAN8 = 68 118 CODE39 = 69 119 ITF = 70 120 CODABAR = 71 121 CODE93 = 72 122 CODE128 = 73 123 # pylint: enable=bad-whitespace 124 125 class _PrintModeBit: 126 # Internal descriptor class to simplify printer mode change properties. 127 # This is tightly coupled to the ThermalPrinter implementation--do not 128 # change it without fully understanding these dependencies on the 129 # internal _set_print_mode and other methods! 130 131 # pylint doesn't have the context to realize this internal class is 132 # explicitly tightly coupled to the parent class implementation. 133 # Therefore disable its warnings about protected access--this access 134 # is required and by design. 135 # pylint: disable=protected-access 136 137 # Another odd pylint case, it seems to not realize this is a descriptor 138 # which by design only implements get, set, init. As a result workaround 139 # this pylint issue by disabling the warning. 140 # pylint: disable=too-few-public-methods 141 def __init__(self, mask): 142 self._mask = mask 143 144 def __get__(self, obj, objtype): 145 return obj._print_mode & self._mask > 0 146 147 def __set__(self, obj, val): 148 if val: 149 obj._set_print_mode(self._mask) 150 else: 151 obj._unset_print_mode(self._mask) 152 153 # pylint: enable=protected-access 154 # pylint: enable=too-few-public-methods 155 156 def __init__( 157 self, 158 uart, 159 *, 160 byte_delay_s=0.00057346, 161 dot_feed_s=0.0021, 162 dot_print_s=0.03, 163 auto_warm_up=True 164 ): 165 """Thermal printer class. Requires a serial UART connection with at 166 least the TX pin connected. Take care connecting RX as the printer 167 will output a 5V signal which can damage boards! If RX is unconnected 168 the only loss in functionality is the has_paper function, all other 169 printer functions will continue to work. The byte_delay_s, dot_feed_s, 170 and dot_print_s values are delays which are used to prevent overloading 171 the printer with data. Use the default delays unless you fully 172 understand the workings of the printer and how delays, baud rate, 173 number of dots, heat time, etc. relate to each other. Can set 174 auto_warm_up to a boolean value (default True) to automatically call 175 the warm_up function which will initialize the printer (but can take a 176 significant amount of time, on the order 0.5-5 seconds, be warned!). 177 """ 178 self.max_chunk_height = 255 179 self._resume = 0 180 self._uart = uart 181 self._print_mode = 0 182 self._column = 0 183 self._max_column = 32 184 self._char_height = 24 185 self._line_spacing = 6 186 self._barcode_height = 50 187 # pylint: disable=line-too-long 188 # Byte delay calculated based on assumption of 19200 baud. 189 # From Arduino library code, see formula here: 190 # https://github.com/adafruit/Adafruit-Thermal-Printer-Library/blob/master/Adafruit_Thermal.cpp#L50-L53 191 # pylint: enable=line-too-long 192 self._byte_delay_s = byte_delay_s 193 self._dot_feed_s = dot_feed_s 194 self._dot_print_s = dot_print_s 195 self.reset() 196 if auto_warm_up: 197 self.warm_up() 198 199 def _set_timeout(self, period_s): 200 # Set a timeout before future commands can be sent. 201 self._resume = time.monotonic() + period_s 202 203 def _wait_timeout(self): 204 # Ensure the timeout that was previously set has passed (will busy wait). 205 while time.monotonic() < self._resume: 206 pass 207 208 def _write_char(self, char): 209 # Write a single character to the printer. 210 if char == "\r": 211 return # Strip carriage returns by skipping them. 212 self._wait_timeout() 213 self._uart.write(bytes(char, "ascii")) 214 delay = self._byte_delay_s 215 # Add extra delay for newlines or moving past the last column. 216 if char == "\n" or self._column == self._max_column: 217 if self._column == 0: 218 # Feed line delay 219 delay += (self._char_height + self._line_spacing) * self._dot_feed_s 220 else: 221 # Text line delay 222 delay += (self._char_height * self._dot_print_s) + ( 223 self._line_spacing * self._dot_feed_s 224 ) 225 self._column = 0 226 else: 227 self._column += 1 228 self._set_timeout(delay) 229 230 def _write_print_mode(self): 231 # Write the printer mode to the printer. 232 self.send_command( 233 "\x1B!{0}".format(chr(self._print_mode)) 234 ) # ESC + '!' + print mode byte 235 # Adjust character height and column count based on print mode. 236 self._char_height = 48 if self._print_mode & _DOUBLE_HEIGHT_MASK else 24 237 self._max_column = 16 if self._print_mode & _DOUBLE_WIDTH_MASK else 32 238 239 def _set_print_mode(self, mask): 240 # Enable the specified bits of the print mode. 241 self._print_mode |= mask & 0xFF 242 self._write_print_mode() 243 244 def _unset_print_mode(self, mask): 245 # Disable the specified bits of the print mode. 246 self._print_mode &= ~(mask & 0xFF) 247 self._write_print_mode() 248 249 def send_command(self, command): 250 """Send a command string to the printer.""" 251 self._uart.write(bytes(command, "ascii")) 252 253 # Do initialization in warm_up instead of the initializer because this 254 # initialization takes a long time (5 seconds) and shouldn't happen during 255 # object creation (users need explicit control of when to start it). 256 def warm_up(self, heat_time=120): 257 """Initialize the printer. Can specify an optional heat_time keyword 258 to override the default heating timing of 1.2 ms. See the datasheet 259 for details on the heating time value (duration in 10uS increments). 260 Note that calling this function will take about half a second for the 261 printer to intialize and warm up. 262 """ 263 assert 0 <= heat_time <= 255 264 self._set_timeout(0.5) # Half second delay for printer to initialize. 265 self.reset() 266 # ESC 7 n1 n2 n3 Setting Control Parameter Command 267 # n1 = "max heating dots" 0-255 -- max number of thermal print head 268 # elements that will fire simultaneously. Units = 8 dots (minus 1). 269 # Printer default is 7 (64 dots, or 1/6 of 384-dot width), this code 270 # sets it to 11 (96 dots, or 1/4 of width). 271 # n2 = "heating time" 3-255 -- duration that heating dots are fired. 272 # Units = 10 us. Printer default is 80 (800 us), this code sets it 273 # to value passed (default 120, or 1.2 ms -- a little longer than 274 # the default because we've increased the max heating dots). 275 # n3 = "heating interval" 0-255 -- recovery time between groups of 276 # heating dots on line; possibly a function of power supply. 277 # Units = 10 us. Printer default is 2 (20 us), this code sets it 278 # to 40 (throttled back due to 2A supply). 279 # More heating dots = more peak current, but faster printing speed. 280 # More heating time = darker print, but slower printing speed and 281 # possibly paper 'stiction'. More heating interval = clearer print, 282 # but slower printing speed. 283 # Send ESC + '7' (print settings) + heating dots, heat time, heat interval. 284 self.send_command("\x1B7\x0B{0}\x28".format(chr(heat_time))) 285 # Print density description from manual: 286 # DC2 # n Set printing density 287 # D4..D0 of n is used to set the printing density. Density is 288 # 50% + 5% * n(D4-D0) printing density. 289 # D7..D5 of n is used to set the printing break time. Break time 290 # is n(D7-D5)*250us. 291 print_density = 10 # 100% (? can go higher, text is darker but fuzzy) 292 print_break_time = 2 # 500 uS 293 dc2_value = (print_break_time << 5) | print_density 294 self.send_command("\x12#{0}".format(chr(dc2_value))) # DC2 + '#' + value 295 296 def reset(self): 297 """Reset the printer.""" 298 # Issue a reset command to the printer. (ESC + @) 299 self.send_command("\x1B@") 300 # Reset internal state: 301 self._column = 0 302 self._max_column = 32 303 self._char_height = 24 304 self._line_spacing = 6 305 self._barcode_height = 50 306 # Configure tab stops on recent printers. 307 # ESC + 'D' + tab stop value list ending with null to terminate. 308 self.send_command("\x1BD\x04\x08\x10\x14\x18\x1C\x00") 309 310 def print(self, text, end="\n"): 311 """Print a line of text. Optionally specify the end keyword to 312 override the new line printed after the text (set to None to disable 313 the new line entirely). 314 """ 315 for char in text: 316 self._write_char(char) 317 if end is not None: 318 self._write_char(end) 319 320 def print_barcode(self, text, barcode_type): 321 """Print a barcode with the specified text/number (the meaning 322 varies based on the type of barcode) and type. Type is a value from 323 the datasheet or class-level variables like UPC_A, etc. for 324 convenience. Note the type value changes depending on the firmware 325 version so use class-level values where possible! 326 """ 327 assert 0 <= barcode_type <= 255 328 assert 0 <= len(text) <= 255 329 self.feed(1) # Recent firmware can't print barcode w/o feed first??? 330 self.send_command("\x1DH\x02") # Print label below barcode 331 self.send_command("\x1Dw\x03") # Barcode width 3 (0.375/1.0mm thin/thick) 332 self.send_command("\x1Dk{0}".format(chr(barcode_type))) # Barcode type 333 # Write length and then string (note this only works with 2.64+). 334 self.send_command(chr(len(text))) 335 self.send_command(text) 336 self._set_timeout((self._barcode_height + 40) * self._dot_print_s) 337 self._column = 0 338 339 def _print_bitmap(self, width, height, data): 340 """Print a bitmap image of the specified width, height and data bytes. 341 Data bytes must be in 1-bit per pixel format, i.e. each byte represents 342 8 pixels of image data along a row of the image. You will want to 343 pre-process your images with a script, you CANNOT send .jpg/.bmp/etc. 344 image formats. See this Processing sketch for preprocessing: 345 https://github.com/adafruit/Adafruit-Thermal-Printer-Library/blob/master/processing/bitmapImageConvert/bitmapImageConvert.pde 346 347 .. note:: This is currently not working because it appears the bytes are 348 sent too slowly and the printer gets confused with not enough data being 349 sent to it in the expected time. 350 """ 351 assert len(data) >= (width // 8) * height 352 row_bytes = (width + 7) // 8 # Round up to next byte boundary. 353 row_bytes_clipped = min(row_bytes, 48) # 384 pixels max width. 354 chunk_height_limit = 256 // row_bytes_clipped 355 # Clip chunk height within the 1 to max range. 356 chunk_height_limit = max(1, min(self.max_chunk_height, chunk_height_limit)) 357 i = 0 358 for row_start in range(0, height, chunk_height_limit): 359 # Issue up to chunkHeightLimit rows at a time. 360 chunk_height = min(height - row_start, chunk_height_limit) 361 self.send_command( 362 "\x12*{0}{1}".format(chr(chunk_height), chr(row_bytes_clipped)) 363 ) 364 for _ in range(chunk_height): 365 for _ in range(row_bytes_clipped): 366 # Drop down to low level UART access to avoid newline and 367 # other bitmap values being misinterpreted. 368 self._wait_timeout() 369 self._uart.write(chr(data[i])) 370 i += 1 371 i += row_bytes - row_bytes_clipped 372 self._set_timeout(chunk_height * self._dot_print_s) 373 self._column = 0 374 375 def test_page(self): 376 """Print a test page.""" 377 self.send_command("\x12T") # DC2 + 'T' for test page 378 # Delay for 26 lines w/text (ea. 24 dots high) + 379 # 26 text lines (feed 6 dots) + blank line 380 self._set_timeout( 381 self._dot_print_s * 24 * 26 + self._dot_feed_s * (6 * 26 + 30) 382 ) 383 384 def set_defaults(self): 385 """Set default printing and text options. This is useful to reset back 386 to a good state after printing different size, weight, etc. text. 387 """ 388 self.online() 389 self.justify = JUSTIFY_LEFT 390 self.size = SIZE_SMALL 391 self.underline = None 392 self.inverse = False 393 self.upside_down = False 394 self.double_height = False 395 self.double_width = False 396 self.strike = False 397 self.bold = False 398 self._set_line_height(30) 399 self._set_barcode_height(50) 400 self._set_charset() 401 self._set_code_page() 402 403 def _set_justify(self, val): 404 assert 0 <= val <= 2 405 if val == JUSTIFY_LEFT: 406 self.send_command("\x1Ba\x00") # ESC + 'a' + 0 407 elif val == JUSTIFY_CENTER: 408 self.send_command("\x1Ba\x01") # ESC + 'a' + 1 409 elif val == JUSTIFY_RIGHT: 410 self.send_command("\x1Ba\x02") # ESC + 'a' + 2 411 412 # pylint: disable=line-too-long 413 # Write-only property, can't assume we can read state from the printer 414 # since there is no command for it and hooking up RX is discouraged 415 # (5V will damage many boards). 416 justify = property( 417 None, 418 _set_justify, 419 None, 420 "Set the justification of text, must be a value of JUSTIFY_LEFT, JUSTIFY_CENTER, or JUSTIFY_RIGHT.", 421 ) 422 # pylint: enable=line-too-long 423 424 def _set_size(self, val): 425 assert 0 <= val <= 2 426 if val == SIZE_SMALL: 427 self._char_height = 24 428 self._max_column = 32 429 self.send_command("\x1D!\x00") # ASCII GS + '!' + 0x00 430 elif val == SIZE_MEDIUM: 431 self._char_height = 48 432 self._max_column = 32 433 self.send_command("\x1D!\x01") # ASCII GS + '!' + 0x01 434 elif val == SIZE_LARGE: 435 self._char_height = 48 436 self._max_column = 16 437 self.send_command("\x1D!\x11") # ASCII GS + '!' + 0x11 438 self._column = 0 439 440 # pylint: disable=line-too-long 441 # Write-only property, can't assume we can read state from the printer 442 # since there is no command for it and hooking up RX is discouraged 443 # (5V will damage many boards). 444 size = property( 445 None, 446 _set_size, 447 None, 448 "Set the size of text, must be a value of SIZE_SMALL, SIZE_MEDIUM, or SIZE_LARGE.", 449 ) 450 # pylint: enable=line-too-long 451 452 def _set_underline(self, val): 453 assert val is None or (0 <= val <= 1) 454 if val is None: 455 # Turn off underline. 456 self.send_command("\x1B-\x00") # ESC + '-' + 0 457 elif val == UNDERLINE_THIN: 458 self.send_command("\x1B-\x01") # ESC + '-' + 1 459 elif val == UNDERLINE_THICK: 460 self.send_command("\x1B-\x02") # ESC + '-' + 2 461 462 # pylint: disable=line-too-long 463 # Write-only property, can't assume we can read state from the printer 464 # since there is no command for it and hooking up RX is discouraged 465 # (5V will damage many boards). 466 underline = property( 467 None, 468 _set_underline, 469 None, 470 "Set the underline state of the text, must be None (off), UNDERLINE_THIN, or UNDERLINE_THICK.", 471 ) 472 # pylint: enable=line-too-long 473 474 def _set_inverse(self, inverse): 475 # Set the inverse printing state to enabled disabled with the specified 476 # boolean value. This requires printer firmare 2.68+ 477 if inverse: 478 self.send_command("\x1DB\x01") # ESC + 'B' + 1 479 else: 480 self.send_command("\x1DB\x00") # ESC + 'B' + 0 481 482 # pylint: disable=line-too-long 483 # Write-only property, can't assume we can read inverse state from the 484 # printer since there is no command for it and hooking up RX is discouraged 485 # (5V will damage many boards). 486 inverse = property( 487 None, 488 _set_inverse, 489 None, 490 "Set the inverse printing mode boolean to enable or disable inverse printing.", 491 ) 492 # pylint: enable=line-too-long 493 494 upside_down = _PrintModeBit(_UPDOWN_MASK) 495 496 double_height = _PrintModeBit(_DOUBLE_HEIGHT_MASK) 497 498 double_width = _PrintModeBit(_DOUBLE_WIDTH_MASK) 499 500 strike = _PrintModeBit(_STRIKE_MASK) 501 502 bold = _PrintModeBit(_BOLD_MASK) 503 504 def feed(self, lines): 505 """Advance paper by specified number of blank lines.""" 506 assert 0 <= lines <= 255 507 self.send_command("\x1Bd{0}".format(chr(lines))) 508 self._set_timeout(self._dot_feed_s * self._char_height) 509 self._column = 0 510 511 def feed_rows(self, rows): 512 """Advance paper by specified number of pixel rows.""" 513 assert 0 <= rows <= 255 514 self.send_command("\x1BJ{0}".format(chr(rows))) 515 self._set_timeout(rows * self._dot_feed_s) 516 self._column = 0 517 518 def flush(self): 519 """Flush data pending in the printer.""" 520 self.send_command("\f") 521 522 def offline(self): 523 """Put the printer into an offline state. No other commands can be 524 sent until an online call is made. 525 """ 526 self.send_command("\x1B=\x00") # ESC + '=' + 0 527 528 def online(self): 529 """Put the printer into an online state after previously put offline. 530 """ 531 self.send_command("\x1B=\x01") # ESC + '=' + 1 532 533 def has_paper(self): 534 """Return a boolean indicating if the printer has paper. You MUST have 535 the serial RX line hooked up for this to work. NOTE: be VERY CAREFUL 536 to ensure your board can handle a 5V serial input before hooking up 537 the RX line! 538 """ 539 # This only works with firmware 2.64+: 540 self.send_command("\x1Bv\x00") # ESC + 'v' + 0 541 status = self._uart.read(1) 542 if status is None: 543 return False 544 return not status[0] & 0b00000100 545 546 def _set_line_height(self, height): 547 """Set the line height in pixels. This is the total amount of space 548 between lines, including the height of text. The smallest value is 24 549 and the largest is 255. 550 """ 551 assert 24 <= height <= 255 552 self._line_spacing = height - 24 553 self.send_command("\x1B3{0}".format(chr(height))) # ESC + '3' + height 554 555 def _set_barcode_height(self, height): 556 """Set the barcode height in pixels. Must be a value 1 - 255.""" 557 assert 1 <= height <= 255 558 self._barcode_height = height 559 self.send_command("\x1Dh{0}".format(chr(height))) # ASCII GS + 'h' + height 560 561 def _set_charset(self, charset=0): 562 """Alters the character set for ASCII characters 0x23-0x7E. See 563 datasheet for details on character set values (0-15). Note this is only 564 supported on more recent firmware printers! 565 """ 566 assert 0 <= charset <= 15 567 self.send_command("\x1BR{0}".format(chr(charset))) # ESC + 'R' + charset 568 569 def _set_code_page(self, code_page=0): 570 """Select alternate code page for upper ASCII symbols 0x80-0xFF. See 571 datasheet for code page values (0 - 47). Note this is only supported 572 on more recent firmware printers! 573 """ 574 assert 0 <= code_page <= 47 575 self.send_command("\x1Bt{0}".format(chr(code_page))) # ESC + 't' + code page 576 577 def tab(self): 578 """Print a tab (i.e. move to next 4 character block). Note this is 579 only supported on more recent firmware printers!""" 580 self.send_command("\t") 581 # Increment to the next position that's every 4 spaces. 582 # I.e. increment by 4 and go to the floor/first position of the block. 583 self._column = (self._column + 4) & 0b11111100