summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xaddons/hw_drivers/controllers/driver.py26
-rw-r--r--addons/hw_drivers/drivers/SerialBaseDriver.py144
-rw-r--r--addons/hw_drivers/drivers/SerialScaleDriver.py308
-rw-r--r--addons/hw_posbox_homepage/controllers/main.py16
-rw-r--r--addons/hw_scale/__init__.py4
-rw-r--r--addons/hw_scale/__manifest__.py21
-rw-r--r--addons/hw_scale/controllers/__init__.py4
-rw-r--r--addons/hw_scale/controllers/main.py404
-rw-r--r--addons/hw_scale/i18n/hw_scale.pot16
-rwxr-xr-xaddons/point_of_sale/tools/posbox/overwrite_before_init/etc/init.d/odoo2
10 files changed, 486 insertions, 459 deletions
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() {