summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorJoren Van Onder <jov@odoo.com>2015-12-01 13:04:53 +0100
committerJoren Van Onder <jov@odoo.com>2015-12-09 14:52:10 +0100
commit2bc5d8b32e297154a1f76baebfd28d0e8ef502c2 (patch)
tree67e05169c92cefb1a4e30b5c0aae784a4079d035 /addons
parentc4fb2c2efc8f9c97eb80e616f508476a75eaa109 (diff)
[ADD] hw_blackbox_be: implements the Belgian blackbox protocol
To be used together with the pos_blackbox_be module. The protocol specification is available in: http://www.minfin.fgov.be/portail2/fr/current/spokesperson-12-06-05.htm
Diffstat (limited to 'addons')
-rw-r--r--addons/hw_blackbox_be/__init__.py4
-rw-r--r--addons/hw_blackbox_be/__openerp__.py24
-rw-r--r--addons/hw_blackbox_be/controllers/__init__.py4
-rw-r--r--addons/hw_blackbox_be/controllers/main.py162
4 files changed, 194 insertions, 0 deletions
diff --git a/addons/hw_blackbox_be/__init__.py b/addons/hw_blackbox_be/__init__.py
new file mode 100644
index 00000000000..bf7fb7b45c1
--- /dev/null
+++ b/addons/hw_blackbox_be/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import controllers
diff --git a/addons/hw_blackbox_be/__openerp__.py b/addons/hw_blackbox_be/__openerp__.py
new file mode 100644
index 00000000000..d33930e2143
--- /dev/null
+++ b/addons/hw_blackbox_be/__openerp__.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+{
+ 'name': 'Blackbox Hardware Driver',
+ 'version': '1.0',
+ 'category': 'Hardware Drivers',
+ 'sequence': 6,
+ 'summary': 'Hardware Driver for Belgian Fiscal Data Modules',
+ 'website': 'https://www.odoo.com/page/point-of-sale',
+ 'description': """
+Fiscal Data Module Hardware Driver
+==================================
+
+This module allows a Point Of Sale client to communicate with a
+connected Belgian Fiscal Data Module.
+""",
+ 'author': 'OpenERP SA',
+ 'depends': ['hw_proxy'],
+ 'external_dependencies': {'python': ['serial']},
+ 'test': [
+ ],
+ 'installable': True,
+ 'auto_install': False,
+}
diff --git a/addons/hw_blackbox_be/controllers/__init__.py b/addons/hw_blackbox_be/controllers/__init__.py
new file mode 100644
index 00000000000..f48716d3a69
--- /dev/null
+++ b/addons/hw_blackbox_be/controllers/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import main
diff --git a/addons/hw_blackbox_be/controllers/main.py b/addons/hw_blackbox_be/controllers/main.py
new file mode 100644
index 00000000000..fbc7664cbc0
--- /dev/null
+++ b/addons/hw_blackbox_be/controllers/main.py
@@ -0,0 +1,162 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import logging
+import serial
+from os import listdir
+from threading import Thread, Lock
+
+from openerp import http
+
+import openerp.addons.hw_proxy.controllers.main as hw_proxy
+
+_logger = logging.getLogger(__name__)
+
+class Blackbox(Thread):
+ def __init__(self):
+ Thread.__init__(self)
+ self.blackbox_lock = Lock()
+ self.set_status('connecting')
+ self.device_path = self._find_device_path_by_probing()
+
+ def set_status(self, status, messages=[]):
+ self.status = {
+ 'status': status,
+ 'messages': messages
+ }
+
+ def get_status(self):
+ return self.status
+
+ # There is no real way to find a serial device, all you can really
+ # find is the name of the serial to usb interface, which in the
+ # case of the blackbox is not defined because it doesn't always
+ # come with it's own interface (eg. Retail Cleancash SC-B). So, in
+ # order to differentiate between other devices like this, what
+ # we'll do is probe every serial device with an FDM status
+ # request. The first device to give an answer that makes sense
+ # wins.
+ def _find_device_path_by_probing(self):
+ path = "/dev/serial/by-id/"
+ probe_message = self._wrap_low_level_message_around("S000")
+
+ try:
+ devices = listdir(path)
+ except OSError:
+ _logger.warning(path + " doesn't exist")
+ else:
+ for device in listdir(path):
+ path_to_device = path + device
+ _logger.debug("Probing " + device)
+
+ if self._send_to_blackbox(probe_message, 21, path_to_device, just_wait_for_ack=True):
+ _logger.info(device + " will be used as the blackbox")
+ self.set_status("connected", [device])
+ return path_to_device
+
+ _logger.warning("Blackbox could not be found")
+ self.set_status("error", ["Couldn't find the Fiscal Data Module"])
+ return ""
+
+ def _lrc(self, msg):
+ lrc = 0
+
+ for character in msg:
+ byte = ord(character)
+ lrc = (lrc + byte) & 0xFF
+
+ lrc = ((lrc ^ 0xFF) + 1) & 0xFF
+
+ return lrc
+
+ def _wrap_low_level_message_around(self, high_level_message):
+ bcc = self._lrc(high_level_message)
+ high_level_message_bytes = map(ord, high_level_message)
+
+ low_level_message = bytearray()
+ low_level_message.append(0x02)
+ low_level_message.extend(high_level_message_bytes)
+ low_level_message.append(0x03)
+ low_level_message.append(bcc)
+
+ return low_level_message
+
+ def _send_and_wait_for_ack(self, packet, serial):
+ ack = 0
+ MAX_RETRIES = 1
+
+ while ack != 0x06 and int(chr(packet[4])) < MAX_RETRIES:
+ serial.write(packet)
+ ack = serial.read(1)
+
+ # This violates the principle that we do high level
+ # client-side and low level posbox-side but the retry
+ # counter is always in a fixed position in the high level
+ # message so it's safe to do it. Also it would be a pain
+ # to have to throw this all the way back to js just so it
+ # can increment the retry counter and then try again.
+ packet = packet[:4] + str(int(packet[4]) + 1) + packet[5:]
+
+ if ack:
+ ack = ord(ack)
+ else:
+ _logger.warning("did not get ACK, retrying...")
+ ack = 0
+
+ if ack == 0x06:
+ return True
+ else:
+ _logger.error("retried " + str(MAX_RETRIES) + " times without receiving ACK, is blackbox properly connected?")
+ return False
+
+ def _send_to_blackbox(self, packet, response_size, device_path, just_wait_for_ack=False):
+ if not device_path:
+ return ""
+
+ ser = serial.Serial(port=device_path,
+ baudrate=19200,
+ timeout=3)
+ MAX_NACKS = 1
+ got_response = False
+ sent_nacks = 0
+
+ if self._send_and_wait_for_ack(packet, ser):
+ if just_wait_for_ack:
+ return True
+
+ while not got_response and sent_nacks < MAX_NACKS:
+ stx = ser.read(1)
+ response = ser.read(response_size)
+ etx = ser.read(1)
+ bcc = ser.read(1)
+
+ if stx == chr(0x02) and etx == chr(0x03) and bcc and self._lrc(response) == ord(bcc):
+ got_response = True
+ ser.write(chr(0x06))
+ else:
+ _logger.warning("received ACK but not a valid response, sending NACK...")
+ sent_nacks += 1
+ ser.write(chr(0x15))
+
+ if not got_response:
+ _logger.error("sent " + str(MAX_NACKS) + " NACKS without receiving response, giving up.")
+ return ""
+
+ ser.close()
+ return response
+ else:
+ ser.close()
+ return ""
+
+blackbox_thread = Blackbox()
+hw_proxy.drivers['fiscal_data_module'] = blackbox_thread
+
+class BlackboxDriver(hw_proxy.Proxy):
+ @http.route('/hw_proxy/request_blackbox/', type='json', auth='none', cors='*')
+ def request_blackbox(self, high_level_message, response_size):
+ to_send = blackbox_thread._wrap_low_level_message_around(high_level_message)
+
+ with blackbox_thread.blackbox_lock:
+ response = blackbox_thread._send_to_blackbox(to_send, response_size, blackbox_thread.device_path)
+
+ return response