You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

192 lines
7.2 KiB

  1. # -*- encoding: utf-8 -*-
  2. ##############################################################################
  3. #
  4. # Hardware Customer Display module for Odoo
  5. # Copyright (C) 2014 Akretion (http://www.akretion.com)
  6. # @author Alexis de Lattre <alexis.delattre@akretion.com>
  7. #
  8. # This program is free software: you can redistribute it and/or modify
  9. # it under the terms of the GNU Affero General Public License as
  10. # published by the Free Software Foundation, either version 3 of the
  11. # License, or (at your option) any later version.
  12. #
  13. # This program is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU Affero General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU Affero General Public License
  19. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. #
  21. ##############################################################################
  22. import logging
  23. import json as simplejson
  24. import time
  25. from threading import Thread, Lock
  26. from queue import Queue
  27. import odoo.addons.hw_proxy.controllers.main as hw_proxy
  28. from odoo import http
  29. from odoo.tools.config import config
  30. logger = logging.getLogger(__name__)
  31. CLEAR_DISPLAY = b'\x0C'
  32. MOVE_CURSOR_TO = b'\x1B\x6C'
  33. CURSOR_OFF = b'\x1F\x43\x00'
  34. try:
  35. from serial import Serial
  36. from unidecode import unidecode
  37. except (ImportError, IOError) as err:
  38. logger.debug(err)
  39. class CustomerDisplayDriver(Thread):
  40. def __init__(self):
  41. Thread.__init__(self)
  42. self.queue = Queue()
  43. self.lock = Lock()
  44. self.status = {'status': 'connecting', 'messages': []}
  45. self.device_name = config.get(
  46. 'customer_display_device_name', '/dev/ttyUSB0')
  47. self.device_rate = int(config.get(
  48. 'customer_display_device_rate', 9600))
  49. self.device_timeout = int(config.get(
  50. 'customer_display_device_timeout', 2))
  51. self.serial = False
  52. def get_status(self):
  53. self.push_task('status')
  54. return self.status
  55. def set_status(self, status, message=None):
  56. if status == self.status['status']:
  57. if message is not None and message != self.status['messages'][-1]:
  58. self.status['messages'].append(message)
  59. else:
  60. self.status['status'] = status
  61. if message:
  62. self.status['messages'] = [message]
  63. else:
  64. self.status['messages'] = []
  65. if status == 'error' and message:
  66. logger.error('Display Error: ' + message)
  67. elif status == 'disconnected' and message:
  68. logger.warning('Disconnected Display: ' + message)
  69. def lockedstart(self):
  70. with self.lock:
  71. if not self.isAlive():
  72. self.daemon = True
  73. self.start()
  74. def push_task(self, task, data=None):
  75. self.lockedstart()
  76. self.queue.put((time.time(), task, data))
  77. def move_cursor(self, col, row):
  78. # Bixolon spec : 11. "Move Cursor to Specified Position"
  79. position = MOVE_CURSOR_TO + chr(col).encode('ascii') + chr(row).encode('ascii')
  80. self.cmd_serial_write(position)
  81. def display_text(self, lines):
  82. logger.debug(
  83. "Preparing to send the following lines to LCD: %s" % lines)
  84. # We don't check the number of rows/cols here, because it has already
  85. # been checked in the POS client in the JS code
  86. for row, line in enumerate(lines, start=1):
  87. self.move_cursor(1, row)
  88. self.serial_write(unidecode(line).encode('ascii'))
  89. def setup_customer_display(self):
  90. """Set LCD cursor to off
  91. If your LCD has different setup instruction(s), you should
  92. inherit this function"""
  93. # Bixolon spec : 35. "Set Cursor On/Off"
  94. self.cmd_serial_write(CURSOR_OFF)
  95. logger.debug('LCD cursor set to off')
  96. def clear_customer_display(self):
  97. """If your LCD has different clearing instruction, you should inherit
  98. this function"""
  99. # Bixolon spec : 12. "Clear Display Screen and Clear String Mode"
  100. self.cmd_serial_write(CLEAR_DISPLAY)
  101. logger.debug('Customer display cleared')
  102. def cmd_serial_write(self, command):
  103. """If your LCD requires a prefix and/or suffix on all commands,
  104. you should inherit this function"""
  105. assert isinstance(command, bytes), 'command must be a bytes string'
  106. self.serial_write(command)
  107. def serial_write(self, text):
  108. assert isinstance(text, bytes), 'text must be a bytes string'
  109. self.serial.write(text)
  110. def send_text_customer_display(self, text_to_display):
  111. """This function sends the data to the serial/usb port.
  112. We open and close the serial connection on every message display.
  113. Why ?
  114. 1. Because it is not a problem for the customer display
  115. 2. Because it is not a problem for performance, according to my tests
  116. 3. Because it allows recovery on errors : you can unplug/replug the
  117. customer display and it will work again on the next message without
  118. problem
  119. """
  120. lines = simplejson.loads(text_to_display)
  121. assert isinstance(lines, list), 'lines_list should be a list'
  122. try:
  123. logger.debug(
  124. 'Opening serial port %s for customer display with baudrate %d'
  125. % (self.device_name, self.device_rate))
  126. self.serial = Serial(
  127. self.device_name, self.device_rate,
  128. timeout=self.device_timeout)
  129. logger.debug('serial.is_open = %s' % self.serial.isOpen())
  130. self.setup_customer_display()
  131. self.clear_customer_display()
  132. self.display_text(lines)
  133. except Exception as e:
  134. logger.error('Exception in serial connection: %s' % str(e))
  135. finally:
  136. if self.serial:
  137. logger.debug('Closing serial port for customer display')
  138. self.serial.close()
  139. def run(self):
  140. while True:
  141. try:
  142. timestamp, task, data = self.queue.get(True)
  143. if task == 'display':
  144. self.send_text_customer_display(data)
  145. elif task == 'status':
  146. serial = Serial(
  147. self.device_name, self.device_rate,
  148. timeout=self.device_timeout)
  149. if serial.isOpen():
  150. self.set_status(
  151. 'connected',
  152. 'Connected to %s' % self.device_name
  153. )
  154. self.serial = serial
  155. except Exception as e:
  156. self.set_status('error', str(e))
  157. driver = CustomerDisplayDriver()
  158. hw_proxy.drivers['customer_display'] = driver
  159. class CustomerDisplayProxy(hw_proxy.Proxy):
  160. @http.route(
  161. '/hw_proxy/send_text_customer_display', type='json', auth='none',
  162. cors='*')
  163. def send_text_customer_display(self, text_to_display):
  164. logger.debug(
  165. 'LCD: Call send_text_customer_display with text=%s',
  166. text_to_display)
  167. driver.push_task('display', text_to_display)