[IMP] iot: add iot serial drivers

This commit adds a base serial and a base serial scale iot drivers,
as well as drivers extending them for the two previously supported scales
(ADAM EQUIPMENT AZEXTRA and TOLEDO 8217).

Since just adding the new drivers creates a race condition
between the old drivers and the new, the old drivers need to be removed.

The IoT scale drivers need to be backward compatible and compatible with the community version,
so client using an IoT box with those versions can still connect a scale to the PoS.

Task: 1892412
This commit is contained in:
pha-odoo
2019-07-17 08:02:13 +02:00
parent a0fc2210e6
commit c4aade63fd
10 changed files with 486 additions and 459 deletions
+24 -2
View File
@@ -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
@@ -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()
@@ -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
@@ -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({
-4
View File
@@ -1,4 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import controllers
-21
View File
@@ -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,
}
-4
View File
@@ -1,4 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import main
-404
View File
@@ -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
-16
View File
@@ -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"
@@ -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() {