From 07a495d52c76ebc817c4e61f85f1bf917d5ed7ca Mon Sep 17 00:00:00 2001 From: Antoine Prieels Date: Mon, 19 Oct 2020 09:32:19 +0000 Subject: [PATCH] [IMP] hw_drivers: Support for Star receipt printers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add support for Star Line Mode protocol, used by most Star receipt printers. closes odoo/odoo#60371 Taskid: 2365648 X-original-commit: cd0545b98fc1e7a66dd46eb6a283736ef58ac4ff Signed-off-by: Quentin Lejeune (qle) Signed-off-by: Antoine Prieƫls --- .../iot_handlers/drivers/PrinterDriver.py | 137 +++++++----------- 1 file changed, 54 insertions(+), 83 deletions(-) diff --git a/addons/hw_drivers/iot_handlers/drivers/PrinterDriver.py b/addons/hw_drivers/iot_handlers/drivers/PrinterDriver.py index f3558be545b..bbd79f1cb4e 100644 --- a/addons/hw_drivers/iot_handlers/drivers/PrinterDriver.py +++ b/addons/hw_drivers/iot_handlers/drivers/PrinterDriver.py @@ -25,31 +25,20 @@ from odoo.addons.hw_drivers.tools import helpers _logger = logging.getLogger(__name__) -def print_star_error(deviceId): - """We communicate with receipt printers using the ESCPOS protocol. - By default, Star printers use the Star mode. This can be changed by - modifying the position of the DIP-Switches at the bottom of the printer. - """ - error_page = ( - "\x1B\x1D\x61\x01" # Centering start - "\x1B\x69\x01\x01Bad configuration\x1B\x69\x00\x00" # Title, double size - "\n\n--------------------\n\n" - "\x1B\x1D\x61\x00" # Centering Stop - "Your printer is in Star line mode, but should\n" - "use ESC/POS mode. You will not be able to print\n" - "receipts without changing your configuration.\n\n" - "For more details and instructions on how to\n" - "configure your printer, please refer to:\n\n" - "\x1B\x1D\x61\x01" # Centering start - "\x1B\x2D\x01" # Underline start - "http://www.odoo.com" # TODO: Replace URL - "\x0A\x0A" - "\x1B\x2D\x00" # Underline stop - "\x1B\x1D\x61\x00" # Centering stop - "\x1B\x64\x02" # Full Cut - ) - process = subprocess.Popen(["lp", "-d", deviceId], stdin=subprocess.PIPE) - process.communicate(error_page.encode("utf-8")) +RECEIPT_PRINTER_COMMANDS = { + 'star': { + 'center': b'\x1b\x1d\x61\x01', # ESC GS a n + 'cut': b'\x1b\x64\x02', # ESC d n + 'title': b'\x1b\x69\x01\x01%s\x1b\x69\x00\x00', # ESC i n1 n2 + 'drawers': [b'\x07', b'\x1a'] # BEL & SUB + }, + 'escpos': { + 'center': b'\x1b\x61\x01', # ESC a n + 'cut': b'\x1d\x56\x41\n', # GS V m + 'title': b'\x1b\x21\x30%s\x1b\x21\x00', # ESC ! n + 'drawers': [b'\x1b\x3d\x01', b'\x1b\x70\x00\x19\x19', b'\x1b\x70\x01\x19\x19'] # ESC = n then ESC p m t1 t2 + } +} def cups_notification_handler(message, uri, device_identifier, state, reason, accepting_jobs): if device_identifier in iot_devices: @@ -90,7 +79,9 @@ class PrinterDriver(Driver): 'reason': None, } self.send_status() - if 'direct' in self.device_connection and 'CMD:ESC/POS;' in device['device-id']: + + self.receipt_protocol = 'star' if 'STR_T' in device['device-id'] else 'escpos' + if 'direct' in self.device_connection and any(cmd in device['device-id'] for cmd in ['CMD:STAR;', 'CMD:ESC/POS;']): self.print_status() @classmethod @@ -116,11 +107,7 @@ class PrinterDriver(Driver): conn.setPrinterUsersAllowed(device['identifier'], ['all']) conn.addPrinterOptionDefault(device['identifier'], "usb-no-reattach", "true") conn.addPrinterOptionDefault(device['identifier'], "usb-unidir", "true") - if 'STR_T' in device['device-id']: - # Star printers have either STR_T or ESP in their name depending on the protocol used. - print_star_error(device['identifier']) - else: - return True + return True return False @classmethod @@ -189,52 +176,40 @@ class PrinterDriver(Driver): im = ImageOps.invert(im) im = im.convert("1") - '''ESC a n - - ESC a: Select justification, in hex: "1B 61" - - n: Justification: - - 0: left - - 1: center - - 2: right - ''' - center = b'\x1b\x61\x01' + print_command = getattr(self, 'format_%s' % self.receipt_protocol)(im) + self.print_raw(print_command) - '''GS v0 m x y - - GS v0: Print raster bit image, in hex: "1D 76 30" - - m: Density mode: - - 0: 180dpi x 180dpi - - 1: 180dpi x 90dpi - - 2: 90dpi x 180 dpi - - 3: 90dpi x 90 dpi - - x: Length in X direction, in bytes, represented as 2 bytes in little endian - --> Must be <= 255 - - y: Length in Y direction, in dots, represented as 2 bytes in little endian - ''' - width_pixels, height_pixels = im.size - width_bytes = int((width_pixels + 7) / 8) - print_command = b"\x1d\x76\x30" + b'\x00' + (width_bytes).to_bytes(2, 'little') + height_pixels.to_bytes(2, 'little') + def format_star(self, im): + width = int((im.width + 7) / 8) - # There is a height limit when printing images, so we split the image - # into slices and print each slice with a separate command. - blobs = [] - slice_offset = 0 - while slice_offset < height_pixels: - slice_height_pixels = min(255, height_pixels - slice_offset) - im_slice = im.crop((0, slice_offset, width_pixels, slice_offset + slice_height_pixels)) - print_command = b"\x1d\x76\x30" + b'\x00' + (width_bytes).to_bytes(2, 'little') + slice_height_pixels.to_bytes(2, 'little') - blobs += [print_command + im_slice.tobytes()] - slice_offset += slice_height_pixels + raster_init = b'\x1b\x2a\x72\x41' + raster_page_length = b'\x1b\x2a\x72\x50\x30\x00' + raster_send = b'\x62' + raster_close = b'\x1b\x2a\x72\x42' - '''GS V m - - GS V: Cut, in hex: "1D 56" - - m: Cut mode: - - 0: Full cut - - 1: Partial cut - - 65 (0x41): Feed paper then full cut - - 66 (0x42): Feed paper then partial cut - ''' - cut = b'\x1d\x56' + b'\x41' + raster_data = b'' + dots = im.tobytes() + while len(dots): + raster_data += raster_send + width.to_bytes(2, 'little') + dots[:width] + dots = dots[width:] - self.print_raw(center + b"".join(blobs) + cut + b'\n') + return raster_init + raster_page_length + raster_data + raster_close + + def format_escpos(self, im): + width = int((im.width + 7) / 8) + + raster_send = b'\x1d\x76\x30\x00' + max_slice_height = 255 + + raster_data = b'' + dots = im.tobytes() + while len(dots): + im_slice = dots[:width*max_slice_height] + slice_height = int(len(im_slice) / width) + raster_data += raster_send + width.to_bytes(2, 'little') + slice_height.to_bytes(2, 'little') + im_slice + dots = dots[width*max_slice_height:] + + return raster_data + RECEIPT_PRINTER_COMMANDS['escpos']['cut'] def print_status(self): """Prints the status ticket of the IoTBox on the current printer.""" @@ -272,19 +247,15 @@ class PrinterDriver(Driver): if code: pairing_code = '\nPairing Code:\n%s\n' % code - center = b'\x1b\x61\x01' - title = b'\n\x1b\x21\x30\x1b\x4d\x01IoTBox Status\x1b\x4d\x00\x1b\x21\x00\n' - cut = b'\x1d\x56\x41' - - self.print_raw(center + title + wlan.encode() + mac.encode() + ip.encode() + homepage.encode() + pairing_code.encode() + cut + b'\n') + commands = RECEIPT_PRINTER_COMMANDS[self.receipt_protocol] + title = commands['title'] % b'IoTBox Status' + self.print_raw(commands['center'] + title + b'\n' + wlan.encode() + mac.encode() + ip.encode() + homepage.encode() + pairing_code.encode() + commands['cut']) def open_cashbox(self): """Sends a signal to the current printer to open the connected cashbox.""" - # ESC = --> Set peripheral device - self.print_raw(b'\x1b\x3d\x01') - for drawer in [b'\x1b\x70\x00', b'\x1b\x70\x01']: # Kick pin 2 and 5 - command = drawer + b'\x19\x19' # Pulse ON during 50ms then OFF during 50ms - self.print_raw(command) + commands = RECEIPT_PRINTER_COMMANDS[self.receipt_protocol] + for drawer in commands['drawers']: + self.print_raw(drawer) class PrinterController(http.Controller):