diff --git a/addons/hw_drivers/controllers/driver.py b/addons/hw_drivers/controllers/driver.py index cb191e66b10..e97a55427d5 100755 --- a/addons/hw_drivers/controllers/driver.py +++ b/addons/hw_drivers/controllers/driver.py @@ -18,6 +18,7 @@ from cups import Connection as cups_connection from glob import glob from base64 import b64decode from pathlib import Path +from serial.tools.list_ports import comports import socket from odoo import http, _ @@ -124,7 +125,18 @@ iot_devices = {} class DriverMetaClass(type): def __new__(cls, clsname, bases, attrs): newclass = super(DriverMetaClass, cls).__new__(cls, clsname, bases, attrs) - drivers.append(newclass) + # We only add concrete classes to `drivers`. + try: + newclass.supported('test') + except NotImplementedError: + return newclass + except Exception: + pass + # Some drivers must be tried only when all the others have been ruled out. These are kept at the bottom of the list. + if newclass.is_tested_last: + drivers.append(newclass) + else: + drivers.insert(0, newclass) return newclass class Driver(Thread, metaclass=DriverMetaClass): @@ -132,6 +144,7 @@ class Driver(Thread, metaclass=DriverMetaClass): Hook to register the driver into the drivers list """ connection_type = "" + is_tested_last = False def __init__(self, device): super(Driver, self).__init__() @@ -171,7 +184,7 @@ class Driver(Thread, metaclass=DriverMetaClass): On specific driver override this method to check if device is supported or not return True or False """ - pass + raise NotImplementedError def get_message(self): return '' @@ -301,6 +314,14 @@ class Manager(Thread): else: _logger.warning('Odoo server not set') + def serial_loop(self): + serial_devices = {} + for dev in comports(): + dev.identifier = dev.location + iot_device = IoTDevice(dev, 'serial') + serial_devices[dev.identifier] = iot_device + return serial_devices + def usb_loop(self): """ Loops over the connected usb devices, assign them an identifier, instantiate @@ -368,6 +389,7 @@ class Manager(Thread): updated_devices.update(self.video_loop()) updated_devices.update(bt_devices) updated_devices.update(socket_devices) + updated_devices.update(self.serial_loop()) if cpt % 40 == 0: printer_devices = self.printer_loop() cpt = 0 diff --git a/addons/hw_drivers/drivers/SerialBaseDriver.py b/addons/hw_drivers/drivers/SerialBaseDriver.py new file mode 100644 index 00000000000..a355636f9a4 --- /dev/null +++ b/addons/hw_drivers/drivers/SerialBaseDriver.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import logging +import time +import traceback +from collections import namedtuple +from threading import Lock +from contextlib import contextmanager + +import serial + +from odoo.tools.translate import _ +from odoo.addons.hw_drivers.controllers.driver import Driver, event_manager + +_logger = logging.getLogger(__name__) + +SerialProtocol = namedtuple( + 'SerialProtocol', + "name baudrate bytesize stopbits parity timeout writeTimeout measureRegexp statusRegexp " + "commandTerminator commandDelay measureDelay newMeasureDelay " + "measureCommand emptyAnswerValid") + + +@contextmanager +def serial_connection(path, protocol, is_probing=False): + """Opens a serial connection to a device and closes it automatically after use. + + :param path: path to the device + :type path: string + :param protocol: an object containing the serial protocol to connect to a device + :type protocol: namedtuple + :param is_probing: a flag thet if set to `True` makes the timeouts longer, defaults to False + :type is_probing: bool, optional + """ + + PROBING_TIMEOUT = 1 + port_config = { + 'baudrate': protocol.baudrate, + 'bytesize': protocol.bytesize, + 'stopbits': protocol.stopbits, + 'parity': protocol.parity, + 'timeout': PROBING_TIMEOUT if is_probing else protocol.timeout, # longer timeouts for probing + 'writeTimeout': PROBING_TIMEOUT if is_probing else protocol.writeTimeout # longer timeouts for probing + } + connection = serial.Serial(path, **port_config) + yield connection + connection.close() + + +class SerialDriver(Driver): + """Abstract base class for serial drivers.""" + + _protocol = None + connection_type = 'serial' + + STATUS_CONNECTED = 'connected' + STATUS_ERROR = 'error' + STATUS_CONNECTING = 'connecting' + + def __init__(self, device): + """ Attributes initialization method for `SerialDriver`. + + :param device: path to the device + :type device: str + """ + + super().__init__(device) + self._actions = { + 'get_status': self._push_status, + } + self._device_connection = 'serial' + self._device_lock = Lock() + self._status = {'status': self.STATUS_CONNECTING, 'message_title': '', 'message_body': ''} + self._set_name() + + def _get_raw_response(connection): + pass + + def _push_status(self): + """Updates the current status and pushes it to the frontend.""" + + self.data['status'] = self._status + event_manager.device_changed(self) + + def _set_name(self): + """Tries to build the device's name based on its type and protocol name but falls back on a default name if that doesn't work.""" + + try: + name = ('%s serial %s' % (self._protocol.name, self._device_type)).title() + except Exception: + name = 'Unknown Serial Device' + self._device_name = name + + def _take_measure(self): + pass + + def _do_action(self, data): + """Helper function that calls a specific action method on the device. + + :param data: the `_actions` key mapped to the action method we want to call + :type data: string + """ + + try: + with self._device_lock: + self._actions[data]() + time.sleep(self._protocol.commandDelay) + except Exception: + msg = _('An error occured while performing action %s on %s') % (data, self.device_name) + _logger.exception(msg) + self._status = {'status': self.STATUS_ERROR, 'message_title': msg, 'message_body': traceback.format_exc()} + self._push_status() + + def action(self, data): + """Establish a connection with the device if needed and have it perform a specific action. + + :param data: the `_actions` key mapped to the action method we want to call + :type data: string + """ + + if self._connection and self._connection.isOpen(): + self._do_action(data) + else: + with serial_connection(self.dev.device, self._protocol) as connection: + self._connection = connection + self._do_action(data) + + def run(self): + """Continuously gets new measures from the device.""" + + try: + with serial_connection(self.dev.device, self._protocol) as connection: + self._connection = connection + self._status['status'] = self.STATUS_CONNECTED + self._push_status() + while True: + self._take_measure() + time.sleep(self._protocol.newMeasureDelay) + except Exception: + msg = _('Error while reading %s') % self.device_name + _logger.exception(msg) + self._status = {'status': self.STATUS_ERROR, 'message_title': msg, 'message_body': traceback.format_exc()} + self._push_status() diff --git a/addons/hw_drivers/drivers/SerialScaleDriver.py b/addons/hw_drivers/drivers/SerialScaleDriver.py new file mode 100644 index 00000000000..d788b606b94 --- /dev/null +++ b/addons/hw_drivers/drivers/SerialScaleDriver.py @@ -0,0 +1,308 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + + +import threading +import logging +import re +import time +from collections import namedtuple + +import serial + +from odoo import http +from odoo.addons.hw_proxy.controllers.main import drivers as old_drivers +from odoo.addons.hw_drivers.controllers.driver import event_manager +from odoo.addons.hw_drivers.drivers.SerialBaseDriver import SerialDriver, SerialProtocol, serial_connection + + +_logger = logging.getLogger(__name__) + +# Only needed to ensure compatibility with older versions of Odoo +ACTIVE_SCALE = None +new_weight_event = threading.Event() + +ScaleProtocol = namedtuple('ScaleProtocol', SerialProtocol._fields + ('zeroCommand', 'tareCommand', 'clearCommand', 'autoResetWeight')) + +# 8217 Mettler-Toledo (Weight-only) Protocol, as described in the scale's Service Manual. +# e.g. here: https://www.manualslib.com/manual/861274/Mettler-Toledo-Viva.html?page=51#manual +# Our recommended scale, the Mettler-Toledo "Ariva-S", supports this protocol on +# both the USB and RS232 ports, it can be configured in the setup menu as protocol option 3. +# We use the default serial protocol settings, the scale's settings can be configured in the +# scale's menu anyway. +Toledo8217Protocol = ScaleProtocol( + name='Toledo 8217', + baudrate=9600, + bytesize=serial.SEVENBITS, + stopbits=serial.STOPBITS_ONE, + parity=serial.PARITY_EVEN, + timeout=1, + writeTimeout=1, + measureRegexp=b"\x02\\s*([0-9.]+)N?\\r", + statusRegexp=b"\x02\\s*(\\?.)\\r", + commandDelay=0.2, + measureDelay=0.5, + newMeasureDelay=0.2, + commandTerminator=b'', + measureCommand=b'W', + zeroCommand=b'Z', + tareCommand=b'T', + clearCommand=b'C', + emptyAnswerValid=False, + autoResetWeight=False, +) + +# The ADAM scales have their own RS232 protocol, usually documented in the scale's manual +# e.g at https://www.adamequipment.com/media/docs/Print%20Publications/Manuals/PDF/AZEXTRA/AZEXTRA-UM.pdf +# https://www.manualslib.com/manual/879782/Adam-Equipment-Cbd-4.html?page=32#manual +# Only the baudrate and label format seem to be configurable in the AZExtra series. +ADAMEquipmentProtocol = ScaleProtocol( + name='Adam Equipment', + baudrate=4800, + bytesize=serial.EIGHTBITS, + stopbits=serial.STOPBITS_ONE, + parity=serial.PARITY_NONE, + timeout=0.2, + writeTimeout=0.2, + measureRegexp=b"\s*([0-9.]+)kg", # LABEL format 3 + KG in the scale settings, but Label 1/2 should work + statusRegexp=None, + commandTerminator=b"\r\n", + commandDelay=0.2, + measureDelay=0.5, + # AZExtra beeps every time you ask for a weight that was previously returned! + # Adding an extra delay gives the operator a chance to remove the products + # before the scale starts beeping. Could not find a way to disable the beeps. + newMeasureDelay=5, + measureCommand=b'P', + zeroCommand=b'Z', + tareCommand=b'T', + clearCommand=None, # No clear command -> Tare again + emptyAnswerValid=True, # AZExtra does not answer unless a new non-zero weight has been detected + autoResetWeight=True, # AZExtra will not return 0 after removing products +) + + +# Ensures compatibility with older versions of Odoo +class ScaleReadOldRoute(http.Controller): + @http.route('/hw_proxy/scale_read', type='json', auth='none', cors='*') + def scale_read(self): + if ACTIVE_SCALE: + return {'weight': ACTIVE_SCALE._scale_read_old_route()} + return None + + +class ScaleDriver(SerialDriver): + """Abstract base class for scale drivers.""" + + def __init__(self, device): + self._device_type = 'scale' + super().__init__(device) + self._set_actions() + self._is_reading = True + + # Ensures compatibility with older versions of Odoo + # Only the last scale connected is kept + global ACTIVE_SCALE + ACTIVE_SCALE = self + old_drivers['scale'] = ACTIVE_SCALE + + # Ensures compatibility with older versions of Odoo + # and allows using the `ProxyDevice` in the point of sale to retrieve the status + def get_status(self): + """Allows `hw_proxy.Proxy` to retrieve the status of the scales""" + + status = self._status + return {'status': status['status'], 'messages': [status['message_title'], ]} + + def _set_actions(self): + """Initializes `self._actions`, a map of action keys sent by the frontend to backend action methods.""" + + self._actions.update({ + 'read_once': self._read_once_action, + 'set_zero': self._set_zero_action, + 'set_tare': self._set_tare_action, + 'clear_tare': self._clear_tare_action, + 'start_reading': self._start_reading_action, + 'stop_reading': self._stop_reading_action, + }) + + def _start_reading_action(self): + """Starts asking for the scale value.""" + self._is_reading = True + + def _stop_reading_action(self): + """Stops asking for the scale value.""" + self._is_reading = False + + def _clear_tare_action(self): + """Clears the scale current tare weight.""" + + # if the protocol has no clear tare command, we can just tare again + clearCommand = self._protocol.clearCommand or self._protocol.tareCommand + self._connection.write(clearCommand + self._protocol.commandTerminator) + + def _read_once_action(self): + """Reads the scale current weight value and pushes it to the frontend.""" + + self._read_weight() + event_manager.device_changed(self) + + def _set_zero_action(self): + """Makes the weight currently applied to the scale the new zero.""" + + self._connection.write(self._protocol.zeroCommand + self._protocol.commandTerminator) + + def _set_tare_action(self): + """Sets the scale's current weight value as tare weight.""" + + self._connection.write(self._protocol.tareCommand + self._protocol.commandTerminator) + + @staticmethod + def _get_raw_response(connection): + """Gets raw bytes containing the updated value of the device. + + :param connection: a connection to the device's serial port + :type connection: pyserial.Serial + :return: the raw response to a weight request + :rtype: str + """ + + answer = [] + while True: + char = connection.read(1) + if not char: + break + else: + answer.append(bytes(char)) + return b''.join(answer) + + def _read_weight(self): + """Asks for a new weight from the scale, checks if it is valid and, if it is, makes it the current value.""" + + protocol = self._protocol + self._connection.write(protocol.measureCommand + protocol.commandTerminator) + answer = self._get_raw_response(self._connection) + match = re.search(self._protocol.measureRegexp, answer) + self.data = { + 'value': float(match.group(1)) if match else None, + 'status': self._status + } + + # Ensures compatibility with older versions of Odoo + def _scale_read_old_route(self): + """Used when the iot app is not installed""" + with self._device_lock: + self._read_weight() + return self.data['value'] + + def _take_measure(self): + """Reads the device's weight value, and pushes that value to the frontend.""" + + with self._device_lock: + self._read_weight() + if self.data['value'] is not None or self._status['status'] == self.STATUS_ERROR: + event_manager.device_changed(self) + + +class Toledo8217Driver(ScaleDriver): + """Driver for the Toldedo 8217 serial scale.""" + _protocol = Toledo8217Protocol + + @classmethod + def supported(cls, device): + """Checks whether the device, which port info is passed as argument, is supported by the driver. + + :param device: path to the device + :type device: str + :return: whether the device is supported by the driver + :rtype: bool + """ + + protocol = cls._protocol + + try: + with serial_connection(device.device, protocol, is_probing=True) as connection: + connection.write(b'Ehello' + protocol.commandTerminator) + time.sleep(protocol.commandDelay) + answer = connection.read(8) + if answer == b'\x02E\rhello': + connection.write(b'F' + protocol.commandTerminator) + return True + except serial.serialutil.SerialTimeoutException: + pass + except Exception: + _logger.exception('Error while probing %s with protocol %s' % (device, protocol.name)) + return False + + +class AdamEquipmentDriver(ScaleDriver): + """Driver for the Adam Equipment serial scale.""" + + _protocol = ADAMEquipmentProtocol + is_tested_last = True + + def __init__(self, device): + super().__init__(device) + self._is_reading = False + self._last_weight_time = 0 + + def _check_last_weight_time(self): + """The ADAM doesn't make the difference between a value of 0 and "the same value as last time": + in both cases it returns an empty string. + With this, unless the weight changes, we give the user `TIME_WEIGHT_KEPT` seconds to log the new weight, + then change it back to zero to avoid keeping it indefinetely, which could cause issues. + In any case the ADAM must always go back to zero before it can weight again. + """ + + TIME_WEIGHT_KEPT = 10 + + if self.data['value'] is None: + if time.time() - self._last_weight_time > TIME_WEIGHT_KEPT: + self.data['value'] = 0 + else: + self._last_weight_time = time.time() + + def _take_measure(self): + """Reads the device's weight value, and pushes that value to the frontend.""" + + if self._is_reading: + with self._device_lock: + self._read_weight() + self._check_last_weight_time() + if self.data['value'] is not None or self._status['status'] == self.STATUS_ERROR: + event_manager.device_changed(self) + else: + time.sleep(0.5) + + # Ensures compatibility with older versions of Odoo + def _scale_read_old_route(self): + """Used when the iot app is not installed""" + + time.sleep(3) + with self._device_lock: + self._read_weight() + self._check_last_weight_time() + return self.data['value'] + + @classmethod + def supported(cls, device): + """Checks whether the device at `device` is supported by the driver. + + :param device: path to the device + :type device: str + :return: whether the device is supported by the driver + :rtype: bool + """ + + protocol = cls._protocol + + try: + with serial_connection(device.device, protocol, is_probing=True) as connection: + connection.write(protocol.measureCommand + protocol.commandTerminator) + # Checking whether writing to the serial port using the Adam protocol raises a timeout exception is about the only thing we can do. + return True + except serial.serialutil.SerialTimeoutException: + pass + except Exception: + _logger.exception('Error while probing %s with protocol %s' % (device, protocol.name)) + return False diff --git a/addons/hw_posbox_homepage/controllers/main.py b/addons/hw_posbox_homepage/controllers/main.py index 538b6acbf42..36a22356327 100644 --- a/addons/hw_posbox_homepage/controllers/main.py +++ b/addons/hw_posbox_homepage/controllers/main.py @@ -90,13 +90,15 @@ class IoTboxHomepage(web.Home): pos_device = self.get_pos_device_status() iot_device = [] - for status in pos_device: - if pos_device[status]['status'] == 'connected': - iot_device.append({ - 'name': status, - 'type': 'device', - 'message': ' '.join(pos_device[status]['messages']) - }) + + if not iot_devices: + for status in pos_device: + if pos_device[status]['status'] == 'connected': + iot_device.append({ + 'name': status, + 'type': 'device', + 'message': ' '.join(pos_device[status]['messages']) + }) for device in iot_devices: iot_device.append({ diff --git a/addons/hw_scale/__init__.py b/addons/hw_scale/__init__.py deleted file mode 100644 index dd15b5c32e5..00000000000 --- a/addons/hw_scale/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from . import controllers diff --git a/addons/hw_scale/__manifest__.py b/addons/hw_scale/__manifest__.py deleted file mode 100644 index b3e0cb011bb..00000000000 --- a/addons/hw_scale/__manifest__.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -{ - 'name': 'Weighing Scale Hardware Driver', - 'category': 'Point of Sale', - 'sequence': 6, - 'summary': 'Hardware Driver for Weighing Scales', - 'website': 'https://www.odoo.com/page/point-of-sale-hardware', - 'description': """ -Weighing Scale Hardware Driver -================================ - -This module allows the point of sale to connect to a scale using a USB HSM Serial Scale Interface, -such as the Mettler Toledo Ariva. - -""", - 'depends': ['hw_proxy'], - 'external_dependencies': {'python': ['serial']}, - 'installable': False, -} diff --git a/addons/hw_scale/controllers/__init__.py b/addons/hw_scale/controllers/__init__.py deleted file mode 100644 index 5d4b25db9c0..00000000000 --- a/addons/hw_scale/controllers/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from . import main diff --git a/addons/hw_scale/controllers/main.py b/addons/hw_scale/controllers/main.py deleted file mode 100644 index 1ebb6e39af2..00000000000 --- a/addons/hw_scale/controllers/main.py +++ /dev/null @@ -1,404 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -import logging -import os -import re -import time - -from collections import namedtuple -from os import listdir -from threading import Event, Lock, Thread - -from odoo import http - -from odoo.addons.hw_proxy.controllers import main as hw_proxy - -_logger = logging.getLogger(__name__) - -DRIVER_NAME = 'scale' - -try: - import serial -except ImportError: - _logger.error('Odoo module hw_scale depends on the pyserial python module') - serial = None - - -def _toledo8217StatusParse(status): - """ Parse a scale's status, returning a `(weight, weight_info)` pair. """ - weight, weight_info = None, None - stat = status[status.index(b'?') + 1] - if stat == 0: - weight_info = 'ok' - else: - weight_info = [] - if stat & 1 : - weight_info.append('moving') - if stat & 1 << 1: - weight_info.append('over_capacity') - if stat & 1 << 2: - weight_info.append('negative') - weight = 0.0 - if stat & 1 << 3: - weight_info.append('outside_zero_capture_range') - if stat & 1 << 4: - weight_info.append('center_of_zero') - if stat & 1 << 5: - weight_info.append('net_weight') - return weight, weight_info - -ScaleProtocol = namedtuple( - 'ScaleProtocol', - "name baudrate bytesize stopbits parity timeout writeTimeout weightRegexp statusRegexp " - "statusParse commandTerminator commandDelay weightDelay newWeightDelay disable " - "weightCommand zeroCommand tareCommand clearCommand emptyAnswerValid autoResetWeight") - -# 8217 Mettler-Toledo (Weight-only) Protocol, as described in the scale's Service Manual. -# e.g. here: https://www.manualslib.com/manual/861274/Mettler-Toledo-Viva.html?page=51#manual -# Our recommended scale, the Mettler-Toledo "Ariva-S", supports this protocol on -# both the USB and RS232 ports, it can be configured in the setup menu as protocol option 3. -# We use the default serial protocol settings, the scale's settings can be configured in the -# scale's menu anyway. -Toledo8217Protocol = ScaleProtocol( - name='Toledo 8217', - baudrate=9600, - bytesize=serial.SEVENBITS, - stopbits=serial.STOPBITS_ONE, - parity=serial.PARITY_EVEN, - timeout=1, - writeTimeout=1, - weightRegexp=b"\x02\\s*([0-9.]+)N?\\r", - statusRegexp=b"\x02\\s*(\\?.)\\r", - statusParse=_toledo8217StatusParse, - commandDelay=0.2, - weightDelay=0.5, - newWeightDelay=0.2, - commandTerminator=b'', - weightCommand=b'W', - zeroCommand=b'Z', - tareCommand=b'T', - clearCommand=b'C', - emptyAnswerValid=False, - autoResetWeight=False, - disable=False -) - -# The ADAM scales have their own RS232 protocol, usually documented in the scale's manual -# e.g at https://www.adamequipment.com/media/docs/Print%20Publications/Manuals/PDF/AZEXTRA/AZEXTRA-UM.pdf -# https://www.manualslib.com/manual/879782/Adam-Equipment-Cbd-4.html?page=32#manual -# Only the baudrate and label format seem to be configurable in the AZExtra series. -ADAMEquipmentProtocol = ScaleProtocol( - name='Adam Equipment', - baudrate=4800, - bytesize=serial.EIGHTBITS, - stopbits=serial.STOPBITS_ONE, - parity=serial.PARITY_NONE, - timeout=0.2, - writeTimeout=0.2, - weightRegexp=b"\s*([0-9.]+)kg", # LABEL format 3 + KG in the scale settings, but Label 1/2 should work - statusRegexp=None, - statusParse=None, - commandTerminator=b"\r\n", - commandDelay=0.2, - weightDelay=0.5, - newWeightDelay=5, # AZExtra beeps every time you ask for a weight that was previously returned! - # Adding an extra delay gives the operator a chance to remove the products - # before the scale starts beeping. Could not find a way to disable the beeps. - weightCommand=b'P', - zeroCommand=b'Z', - tareCommand=b'T', - clearCommand=None, # No clear command -> Tare again - emptyAnswerValid=True, # AZExtra does not answer unless a new non-zero weight has been detected - autoResetWeight=True, # AZExtra will not return 0 after removing products - disable=True -) - - -SCALE_PROTOCOLS = ( - Toledo8217Protocol, - ADAMEquipmentProtocol, # must be listed last, as it supports no probing! -) - -class Scale(Thread): - def __init__(self): - Thread.__init__(self) - self.lock = Lock() - self.scalelock = Lock() - self.status = {'status':'connecting', 'messages':[]} - self.input_dir = '/dev/serial/by-path/' - self.forbidden_dir = '/dev/serial/by-id/' - self.weight = 0 - self.weight_info = 'ok' - self.device = None - self.path_to_scale = '' - self.protocol = None - self.disabled = False - self.weight_request_event = Event() - - def lockedstart(self): - with self.lock: - if not self.isAlive(): - self.daemon = True - self.start() - - def set_status(self, status, message=None): - if status == self.status['status']: - if message is not None and message != self.status['messages'][-1]: - self.status['messages'].append(message) - - if status == 'error' and message: - _logger.error('Scale Error: '+ message) - elif status == 'disconnected' and message: - _logger.warning('Disconnected Scale: '+ message) - else: - self.status['status'] = status - if message: - self.status['messages'] = [message] - else: - self.status['messages'] = [] - - if status == 'error' and message: - _logger.error('Scale Error: '+ message) - elif status == 'disconnected' and message: - _logger.info('Disconnected Scale: %s', message) - - def _get_raw_response(self, connection): - answer = [] - while True: - char = connection.read(1) # may return `bytes` or `str` - if not char: - break - else: - answer.append(bytes(char)) - return b''.join(answer) - - def _parse_weight_answer(self, protocol, answer): - """ Parse a scale's answer to a weighing request, returning - a `(weight, weight_info, status)` pair. - """ - weight, weight_info, status = None, None, None - try: - _logger.debug("Parsing weight [%r]", answer) - if not answer and protocol.emptyAnswerValid: - # Some scales do not return the same value again, but we - # should not clear the weight data, POS may still be reading it - return weight, weight_info, status - - if protocol.statusRegexp and re.search(protocol.statusRegexp, answer): - # parse status to set weight_info - we'll try weighing again later - weight, weight_info = protocol.statusParse(answer) - else: - match = re.search(protocol.weightRegexp, answer) - if match: - weight_text = match.group(1) - try: - weight = float(weight_text) - _logger.info('Weight: %s', weight) - except ValueError: - _logger.exception("Cannot parse weight [%r]", weight_text) - status = 'Invalid weight, please power-cycle the scale' - else: - _logger.error("Cannot parse scale answer [%r]", answer) - status = 'Invalid scale answer, please power-cycle the scale' - except Exception as e: - _logger.exception("Cannot parse scale answer [%r]", answer) - status = ("Could not weigh on scale %s with protocol %s: %s" % - (self.path_to_scale, protocol.name, e)) - return weight, weight_info, status - - def get_device(self): - if self.device: - return self.device - - with hw_proxy.rs232_lock: - try: - if not os.path.exists(self.input_dir): - self.set_status('disconnected', 'No RS-232 device found') - return None - - forbidden_devices = [os.readlink(self.forbidden_dir + d) for d in listdir(self.forbidden_dir) if 'usb-Sylvac_Power_USB_A32DV5VM' in d] # Skip special usb link with Sylvac - devices = [device for device in listdir(self.input_dir) if os.readlink(self.input_dir + device) not in forbidden_devices] - for device in devices: - path = self.input_dir + device - driver = hw_proxy.rs232_devices.get(device) - if driver and driver != DRIVER_NAME: - # belongs to another driver - _logger.info('Ignoring %s, belongs to %s', device, driver) - continue - - for protocol in SCALE_PROTOCOLS: - _logger.info('Probing %s with protocol %s', path, protocol) - connection = serial.Serial(path, - baudrate=protocol.baudrate, - bytesize=protocol.bytesize, - stopbits=protocol.stopbits, - parity=protocol.parity, - timeout=1, # longer timeouts for probing - writeTimeout=1) # longer timeouts for probing - connection.write(protocol.weightCommand + protocol.commandTerminator) - time.sleep(protocol.commandDelay) - answer = self._get_raw_response(connection) - weight, weight_info, status = self._parse_weight_answer(protocol, answer) - if status: - _logger.info('Probing %s: no valid answer to protocol %s', path, protocol.name) - else: - _logger.info('Probing %s: answer looks ok for protocol %s', path, protocol.name) - self.path_to_scale = path - self.protocol = protocol - self.set_status( - 'connected', - 'Connected to %s with %s protocol' % (device, protocol.name) - ) - connection.timeout = protocol.timeout - connection.writeTimeout = protocol.writeTimeout - hw_proxy.rs232_devices[path] = DRIVER_NAME - return connection - - self.set_status('disconnected', 'No supported RS-232 scale found') - except Exception as e: - _logger.exception('Failed probing for scales') - self.set_status('error', 'Failed probing for scales: %s' % e) - return None - - def get_weight(self): - self.disabled = False - self.weight_request_event.set() - self.lockedstart() - return self.weight - - def get_weight_info(self): - self.lockedstart() - return self.weight_info - - def get_status(self): - self.lockedstart() - return self.status - - def read_weight(self): - with self.scalelock: - p = self.protocol - try: - self.device.write(p.weightCommand + p.commandTerminator) - time.sleep(p.commandDelay) - answer = self._get_raw_response(self.device) - weight, weight_info, status = self._parse_weight_answer(p, answer) - if status: - self.set_status('error', status) - self.device = None - else: - if weight is not None: - self.weight = weight - if weight_info is not None: - self.weight_info = weight_info - except Exception as e: - self.set_status( - 'error', - "Could not weigh on scale %s with protocol %s: %s" % - (self.path_to_scale, p.name, e)) - self.device = None - - def set_zero(self): - with self.scalelock: - if self.device: - try: - self.device.write(self.protocol.zeroCommand + self.protocol.commandTerminator) - time.sleep(self.protocol.commandDelay) - except Exception as e: - self.set_status( - 'error', - "Could not zero scale %s with protocol %s: %s" % - (self.path_to_scale, self.protocol.name, e)) - self.device = None - - def set_tare(self): - with self.scalelock: - if self.device: - try: - self.device.write(self.protocol.tareCommand + self.protocol.commandTerminator) - time.sleep(self.protocol.commandDelay) - except Exception as e: - self.set_status( - 'error', - "Could not tare scale %s with protocol %s: %s" % - (self.path_to_scale, self.protocol.name, e)) - self.device = None - - def clear_tare(self): - with self.scalelock: - if self.device: - p = self.protocol - try: - # if the protocol has no clear, we can just tare again - clearCommand = p.clearCommand or p.tareCommand - self.device.write(clearCommand + p.commandTerminator) - time.sleep(p.commandDelay) - except Exception as e: - self.set_status( - 'error', - "Could not clear tare on scale %s with protocol %s: %s" % - (self.path_to_scale, p.name, e)) - self.device = None - - def run(self): - self.device = None - - while True: - if not self.disabled: - if self.device: - old_weight = self.weight - self.read_weight() - if self.weight != old_weight: - _logger.info('New Weight: %s, sleeping %ss', self.weight, self.protocol.newWeightDelay) - time.sleep(self.protocol.newWeightDelay) - if self.weight and self.protocol.autoResetWeight: - self.weight = 0 - else: - _logger.info('Weight: %s, sleeping %ss', self.weight, self.protocol.weightDelay) - time.sleep(self.protocol.weightDelay) - self.disabled = True - else: - with self.scalelock: - self.device = self.get_device() - if not self.device: - # retry later to support "plug and play" - self.weight_request_event.clear() - self.weight_request_event.wait(10) - else: - self.disabled = self.protocol.disable - else: - self.weight_request_event.clear() - self.weight_request_event.wait(10) - - -scale_thread = None -if serial: - scale_thread = Scale() - hw_proxy.drivers[DRIVER_NAME] = scale_thread - -class ScaleDriver(hw_proxy.Proxy): - @http.route('/hw_proxy/scale_read/', type='json', auth='none', cors='*') - def scale_read(self): - if scale_thread: - return {'weight': scale_thread.get_weight(), - 'unit': 'kg', - 'info': scale_thread.get_weight_info()} - return None - - @http.route('/hw_proxy/scale_zero/', type='json', auth='none', cors='*') - def scale_zero(self): - if scale_thread: - scale_thread.set_zero() - return True - - @http.route('/hw_proxy/scale_tare/', type='json', auth='none', cors='*') - def scale_tare(self): - if scale_thread: - scale_thread.set_tare() - return True - - @http.route('/hw_proxy/scale_clear_tare/', type='json', auth='none', cors='*') - def scale_clear_tare(self): - if scale_thread: - scale_thread.clear_tare() - return True diff --git a/addons/hw_scale/i18n/hw_scale.pot b/addons/hw_scale/i18n/hw_scale.pot deleted file mode 100644 index 5d61a0a94b5..00000000000 --- a/addons/hw_scale/i18n/hw_scale.pot +++ /dev/null @@ -1,16 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 9.0\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-09-07 14:39+0000\n" -"PO-Revision-Date: 2015-09-07 14:39+0000\n" -"Last-Translator: <>\n" -"Language-Team: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Plural-Forms: \n" - diff --git a/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init.d/odoo b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init.d/odoo index 4943ce1090b..5db7e33939d 100755 --- a/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init.d/odoo +++ b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init.d/odoo @@ -26,7 +26,7 @@ test -x $DAEMON || exit 0 set -e function _start() { - start-stop-daemon --start --quiet --pidfile $PIDFILE --chuid $USER:$USER --background --make-pidfile --exec $DAEMON -- --config $CONFIG --logfile $LOGFILE --load=web,hw_proxy,hw_posbox_homepage,hw_posbox_upgrade,hw_scale,hw_scanner,hw_escpos,hw_blackbox_be,hw_screen,hw_drivers + start-stop-daemon --start --quiet --pidfile $PIDFILE --chuid $USER:$USER --background --make-pidfile --exec $DAEMON -- --config $CONFIG --logfile $LOGFILE --load=web,hw_proxy,hw_posbox_homepage,hw_posbox_upgrade,hw_scanner,hw_escpos,hw_blackbox_be,hw_screen,hw_drivers } function _stop() {