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.

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