/ 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