summaryrefslogtreecommitdiff
path: root/addons/hw_drivers/iot_handlers/drivers/SerialScaleDriver.py
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/hw_drivers/iot_handlers/drivers/SerialScaleDriver.py
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/hw_drivers/iot_handlers/drivers/SerialScaleDriver.py')
-rw-r--r--addons/hw_drivers/iot_handlers/drivers/SerialScaleDriver.py316
1 files changed, 316 insertions, 0 deletions
diff --git a/addons/hw_drivers/iot_handlers/drivers/SerialScaleDriver.py b/addons/hw_drivers/iot_handlers/drivers/SerialScaleDriver.py
new file mode 100644
index 00000000..cc1d5469
--- /dev/null
+++ b/addons/hw_drivers/iot_handlers/drivers/SerialScaleDriver.py
@@ -0,0 +1,316 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from collections import namedtuple
+import logging
+import re
+import serial
+import threading
+import time
+
+from odoo import http
+from odoo.addons.hw_drivers.controllers.proxy import proxy_drivers
+from odoo.addons.hw_drivers.event_manager import event_manager
+from odoo.addons.hw_drivers.iot_handlers.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."""
+ last_sent_value = None
+
+ def __init__(self, identifier, device):
+ super(ScaleDriver, self).__init__(identifier, device)
+ self.device_type = 'scale'
+ 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
+ proxy_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, data):
+ """Starts asking for the scale value."""
+ self._is_reading = True
+
+ def _stop_reading_action(self, data):
+ """Stops asking for the scale value."""
+ self._is_reading = False
+
+ def _clear_tare_action(self, data):
+ """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, data):
+ """Reads the scale current weight value and pushes it to the frontend."""
+
+ self._read_weight()
+ self.last_sent_value = self.data['value']
+ event_manager.device_changed(self)
+
+ def _set_zero_action(self, data):
+ """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, data):
+ """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)
+ if match:
+ self.data = {
+ 'value': float(match.group(1)),
+ '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'] != self.last_sent_value or self._status['status'] == self.STATUS_ERROR:
+ self.last_sent_value = self.data['value']
+ event_manager.device_changed(self)
+
+
+class Toledo8217Driver(ScaleDriver):
+ """Driver for the Toldedo 8217 serial scale."""
+ _protocol = Toledo8217Protocol
+
+ def __init__(self, identifier, device):
+ super(Toledo8217Driver, self).__init__(identifier, device)
+ self.device_manufacturer = 'Toledo'
+
+ @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['identifier'], 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
+ priority = 0 # Test the supported method of this driver last, after all other serial drivers
+
+ def __init__(self, identifier, device):
+ super(AdamEquipmentDriver, self).__init__(identifier, device)
+ self._is_reading = False
+ self._last_weight_time = 0
+ self.device_manufacturer = 'Adam'
+
+ 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'] != self.last_sent_value or self._status['status'] == self.STATUS_ERROR:
+ self.last_sent_value = self.data['value']
+ 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['identifier'], 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