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.

190 lines
6.8 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 simplejson
  24. import time
  25. from threading import Thread, Lock
  26. from Queue import Queue
  27. import openerp.addons.hw_proxy.controllers.main as hw_proxy
  28. from openerp import http
  29. from openerp.tools.config import config
  30. logger = logging.getLogger(__name__)
  31. try:
  32. from serial import Serial
  33. from unidecode import unidecode
  34. except (ImportError, IOError) as err:
  35. logger.debug(err)
  36. try:
  37. from unidecode import unidecode
  38. except ImportError:
  39. logger.info('`unidecode` Python pacakge not found')
  40. class CustomerDisplayDriver(Thread):
  41. def __init__(self):
  42. Thread.__init__(self)
  43. self.queue = Queue()
  44. self.lock = Lock()
  45. self.status = {'status': 'connecting', 'messages': []}
  46. self.device_name = config.get(
  47. 'customer_display_device_name', '/dev/ttyUSB0')
  48. self.device_rate = int(config.get(
  49. 'customer_display_device_rate', 9600))
  50. self.device_timeout = int(config.get(
  51. 'customer_display_device_timeout', 2))
  52. self.serial = False
  53. def get_status(self):
  54. self.push_task('status')
  55. return self.status
  56. def set_status(self, status, message=None):
  57. if status == self.status['status']:
  58. if message is not None and message != self.status['messages'][-1]:
  59. self.status['messages'].append(message)
  60. else:
  61. self.status['status'] = status
  62. if message:
  63. self.status['messages'] = [message]
  64. else:
  65. self.status['messages'] = []
  66. if status == 'error' and message:
  67. logger.error('Display Error: '+message)
  68. elif status == 'disconnected' and message:
  69. logger.warning('Disconnected Display: '+message)
  70. def lockedstart(self):
  71. with self.lock:
  72. if not self.isAlive():
  73. self.daemon = True
  74. self.start()
  75. def push_task(self, task, data=None):
  76. self.lockedstart()
  77. self.queue.put((time.time(), task, data))
  78. def move_cursor(self, col, row):
  79. # Bixolon spec : 11. "Move Cursor to Specified Position"
  80. self.cmd_serial_write('\x1B\x6C' + chr(col) + chr(row))
  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. lines_ascii = []
  87. for line in lines:
  88. lines_ascii.append(unidecode(line))
  89. row = 0
  90. for dline in lines_ascii:
  91. row += 1
  92. self.move_cursor(1, row)
  93. self.serial_write(dline)
  94. def setup_customer_display(self):
  95. '''Set LCD cursor to off
  96. If your LCD has different setup instruction(s), you should
  97. inherit this function'''
  98. # Bixolon spec : 35. "Set Cursor On/Off"
  99. self.cmd_serial_write('\x1F\x43\x00')
  100. logger.debug('LCD cursor set to off')
  101. def clear_customer_display(self):
  102. '''If your LCD has different clearing instruction, you should inherit
  103. this function'''
  104. # Bixolon spec : 12. "Clear Display Screen and Clear String Mode"
  105. self.cmd_serial_write('\x0C')
  106. logger.debug('Customer display cleared')
  107. def cmd_serial_write(self, command):
  108. '''If your LCD requires a prefix and/or suffix on all commands,
  109. you should inherit this function'''
  110. assert isinstance(command, str), 'command must be a string'
  111. self.serial_write(command)
  112. def serial_write(self, text):
  113. assert isinstance(text, str), 'text must be a string'
  114. self.serial.write(text)
  115. def send_text_customer_display(self, text_to_display):
  116. '''This function sends the data to the serial/usb port.
  117. We open and close the serial connection on every message display.
  118. Why ?
  119. 1. Because it is not a problem for the customer display
  120. 2. Because it is not a problem for performance, according to my tests
  121. 3. Because it allows recovery on errors : you can unplug/replug the
  122. customer display and it will work again on the next message without
  123. problem
  124. '''
  125. lines = simplejson.loads(text_to_display)
  126. assert isinstance(lines, list), 'lines_list should be a list'
  127. try:
  128. logger.debug(
  129. 'Opening serial port %s for customer display with baudrate %d'
  130. % (self.device_name, self.device_rate))
  131. self.serial = Serial(
  132. self.device_name, self.device_rate,
  133. timeout=self.device_timeout)
  134. logger.debug('serial.is_open = %s' % self.serial.isOpen())
  135. self.setup_customer_display()
  136. self.clear_customer_display()
  137. self.display_text(lines)
  138. except Exception, e:
  139. logger.error('Exception in serial connection: %s' % str(e))
  140. finally:
  141. if self.serial:
  142. logger.debug('Closing serial port for customer display')
  143. self.serial.close()
  144. def run(self):
  145. while True:
  146. try:
  147. timestamp, task, data = self.queue.get(True)
  148. if task == 'display':
  149. self.send_text_customer_display(data)
  150. elif task == 'status':
  151. pass
  152. except Exception as e:
  153. self.set_status('error', str(e))
  154. driver = CustomerDisplayDriver()
  155. hw_proxy.drivers['customer_display'] = driver
  156. class CustomerDisplayProxy(hw_proxy.Proxy):
  157. @http.route(
  158. '/hw_proxy/send_text_customer_display', type='json', auth='none',
  159. cors='*')
  160. def send_text_customer_display(self, text_to_display):
  161. logger.debug(
  162. 'LCD: Call send_text_customer_display with text=%s',
  163. text_to_display)
  164. driver.push_task('display', text_to_display)