summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorAntoine Prieels <anp@odoo.com>2020-05-14 07:45:07 +0000
committerAntoine Prieels <anp@odoo.com>2020-08-25 06:19:38 +0000
commit841c016913a87133bf7257c62ae5f6bf7d99e06d (patch)
treeb0ed83f8b440e9920ec32c4dd48818847c4725d6 /addons
parent6bc4b4178c8033f19f1ed3d7ba22f777ac9b0788 (diff)
[IMP] hw_*,point_of_sale: Cleaning IoT Box
- Split driver.py to multiple files - Remove hw_proxy module as most of its routes were not used and move remaingin routes to a controller in hw_drivers - Add missing coding and license lines at the beginning of files - Reorder imports and remove unused ones - Rename device_id to device_identifier for clarity - Rename screen to display for consistency - Other minors improvements TaskID: 1973956
Diffstat (limited to 'addons')
-rw-r--r--addons/hw_drivers/__init__.py11
-rw-r--r--addons/hw_drivers/connection_manager.py74
-rw-r--r--addons/hw_drivers/controllers/__init__.py6
-rwxr-xr-xaddons/hw_drivers/controllers/driver.py382
-rw-r--r--addons/hw_drivers/controllers/proxy.py22
-rw-r--r--addons/hw_drivers/driver.py51
-rw-r--r--addons/hw_drivers/event_manager.py47
-rw-r--r--addons/hw_drivers/exception_logger.py23
-rw-r--r--addons/hw_drivers/interface.py59
-rw-r--r--addons/hw_drivers/iot_handlers/drivers/DisplayDriver.py27
-rw-r--r--addons/hw_drivers/iot_handlers/drivers/KeyboardUSBDriver.py43
-rw-r--r--addons/hw_drivers/iot_handlers/drivers/PrinterDriver.py43
-rw-r--r--addons/hw_drivers/iot_handlers/drivers/SerialBaseDriver.py32
-rw-r--r--addons/hw_drivers/iot_handlers/drivers/SerialScaleDriver.py32
-rw-r--r--addons/hw_drivers/iot_handlers/interfaces/DisplayInterface.py6
-rw-r--r--addons/hw_drivers/iot_handlers/interfaces/PrinterInterface.py5
-rw-r--r--addons/hw_drivers/iot_handlers/interfaces/SerialInterface.py5
-rw-r--r--addons/hw_drivers/iot_handlers/interfaces/USBInterface.py13
-rw-r--r--addons/hw_drivers/main.py104
-rw-r--r--addons/hw_drivers/tools/helpers.py29
-rw-r--r--addons/hw_escpos/__manifest__.py1
-rw-r--r--addons/hw_escpos/controllers/main.py12
-rw-r--r--addons/hw_posbox_homepage/__manifest__.py1
-rw-r--r--addons/hw_posbox_homepage/controllers/main.py23
-rw-r--r--addons/hw_posbox_homepage/views/homepage.html4
-rw-r--r--addons/hw_posbox_homepage/views/upgrade_page.html2
-rw-r--r--addons/hw_proxy/__init__.py4
-rw-r--r--addons/hw_proxy/__manifest__.py21
-rw-r--r--addons/hw_proxy/controllers/__init__.py4
-rw-r--r--addons/hw_proxy/controllers/main.py218
-rwxr-xr-xaddons/point_of_sale/tools/posbox/overwrite_before_init/etc/init.d/odoo2
-rwxr-xr-xaddons/point_of_sale/tools/posbox/overwrite_before_init/etc/init_posbox_image.sh84
32 files changed, 589 insertions, 801 deletions
diff --git a/addons/hw_drivers/__init__.py b/addons/hw_drivers/__init__.py
index a03bfd097a6..b36b894717b 100644
--- a/addons/hw_drivers/__init__.py
+++ b/addons/hw_drivers/__init__.py
@@ -1 +1,10 @@
-from . import controllers \ No newline at end of file
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import connection_manager
+from . import controllers
+from . import driver
+from . import event_manager
+from . import exception_logger
+from . import interface
+from . import main
diff --git a/addons/hw_drivers/connection_manager.py b/addons/hw_drivers/connection_manager.py
new file mode 100644
index 00000000000..ec8ba076489
--- /dev/null
+++ b/addons/hw_drivers/connection_manager.py
@@ -0,0 +1,74 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from datetime import datetime, timedelta
+import logging
+import subprocess
+import requests
+from threading import Thread
+import time
+import urllib3
+
+from odoo.modules.module import get_resource_path
+from odoo.addons.hw_drivers.main import iot_devices, manager
+from odoo.addons.hw_drivers.tools import helpers
+
+_logger = logging.getLogger(__name__)
+
+class ConnectionManager(Thread):
+ def __init__(self):
+ super(ConnectionManager, self).__init__()
+ self.pairing_code = False
+ self.pairing_uuid = False
+
+ def run(self):
+ if not helpers.get_odoo_server_url():
+ end_time = datetime.now() + timedelta(minutes=5)
+ while (datetime.now() < end_time):
+ self._connect_box()
+ time.sleep(10)
+ self.pairing_code = False
+ self.pairing_uuid = False
+ self._refresh_displays()
+
+ def _connect_box(self):
+ data = {
+ 'jsonrpc': 2.0,
+ 'params': {
+ 'pairing_code': self.pairing_code,
+ 'pairing_uuid': self.pairing_uuid,
+ }
+ }
+
+ urllib3.disable_warnings()
+ req = requests.post('https://iot-proxy.odoo.com/odoo-enterprise/iot/connect-box', json=data, verify=False)
+ result = req.json().get('result', {})
+
+ if all(key in result for key in ['pairing_code', 'pairing_uuid']):
+ self.pairing_code = result['pairing_code']
+ self.pairing_uuid = result['pairing_uuid']
+ elif all(key in result for key in ['url', 'token', 'db_uuid', 'enterprise_code']):
+ self._connect_to_server(result['url'], result['token'], result['db_uuid'], result['enterprise_code'])
+
+ def _connect_to_server(self, url, token, db_uuid, enterprise_code):
+ if db_uuid and enterprise_code:
+ helpers.add_credential(db_uuid, enterprise_code)
+
+ # Save DB URL and token
+ subprocess.check_call([get_resource_path('point_of_sale', 'tools/posbox/configuration/connect_to_server.sh'), url, '', token, 'noreboot'])
+ # Notify the DB, so that the kanban view already shows the IoT Box
+ manager.send_alldevices()
+ # Restart to checkout the git branch, get a certificate, load the IoT handlers...
+ subprocess.check_call(["sudo", "service", "odoo", "restart"])
+
+ def _refresh_displays(self):
+ """Refresh all displays to hide the pairing code"""
+ for d in iot_devices:
+ if iot_devices[d].device_type == 'display':
+ iot_devices[d].action({
+ 'action': 'display_refresh'
+ })
+
+connection_manager = ConnectionManager()
+connection_manager.daemon = True
+connection_manager.start()
diff --git a/addons/hw_drivers/controllers/__init__.py b/addons/hw_drivers/controllers/__init__.py
index 8a59703b3c1..ad287d4d087 100644
--- a/addons/hw_drivers/controllers/__init__.py
+++ b/addons/hw_drivers/controllers/__init__.py
@@ -1 +1,5 @@
-from . import driver \ No newline at end of file
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import driver
+from . import proxy
diff --git a/addons/hw_drivers/controllers/driver.py b/addons/hw_drivers/controllers/driver.py
index 46605790bf2..39ac2408947 100755
--- a/addons/hw_drivers/controllers/driver.py
+++ b/addons/hw_drivers/controllers/driver.py
@@ -1,40 +1,32 @@
-#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
from base64 import b64decode
-from datetime import datetime, timedelta
-from dbus.mainloop.glib import DBusGMainLoop
-from importlib import util
import json
import logging
import os
-import requests
-import socket
import subprocess
-import sys
-from threading import Thread, Event
-import time
-import urllib3
from odoo import http, tools
from odoo.http import send_file
from odoo.modules.module import get_resource_path
+
+from odoo.addons.hw_drivers.event_manager import event_manager
+from odoo.addons.hw_drivers.main import iot_devices, manager
from odoo.addons.hw_drivers.tools import helpers
_logger = logging.getLogger(__name__)
-#----------------------------------------------------------
-# Controllers
-#----------------------------------------------------------
-
-class StatusController(http.Controller):
+class DriverController(http.Controller):
@http.route('/hw_drivers/action', type='json', auth='none', cors='*', csrf=False, save_session=False)
- def action(self, session_id, device_id, data):
+ def action(self, session_id, device_identifier, data):
"""
This route is called when we want to make a action with device (take picture, printing,...)
We specify in data from which session_id that action is called
And call the action of specific device
"""
- iot_device = iot_devices.get(device_id)
+ iot_device = iot_devices.get(device_identifier)
if iot_device:
iot_device.data['owner'] = session_id
data = json.loads(data)
@@ -53,7 +45,7 @@ class StatusController(http.Controller):
@http.route('/hw_drivers/event', type='json', auth='none', cors='*', csrf=False, save_session=False)
def event(self, listener):
"""
- listener is a dict in witch there are a sessions_id and a dict of device_id to listen
+ listener is a dict in witch there are a sessions_id and a dict of device_identifier to listen
"""
req = event_manager.add_request(listener)
if req['event'].wait(50):
@@ -82,7 +74,7 @@ class StatusController(http.Controller):
helpers.add_credential(db_uuid, enterprise_code)
try:
subprocess.check_call([get_resource_path('point_of_sale', 'tools/posbox/configuration/connect_to_server.sh'), url, '', token, 'noreboot'])
- m.send_alldevices()
+ manager.send_alldevices()
image = get_resource_path('hw_drivers', 'static/img', 'True.jpg')
helpers.odoo_restart(3)
except subprocess.CalledProcessError as e:
@@ -100,355 +92,3 @@ class StatusController(http.Controller):
res = send_file(tools.config['logfile'], mimetype="text/plain", as_attachment=True)
res.headers['Cache-Control'] = 'no-cache'
return res
-
-#----------------------------------------------------------
-# Log Exceptions
-#----------------------------------------------------------
-
-class ExceptionLogger:
- """
- Redirect Exceptions to the logger to keep track of them in the log file.
- """
-
- def __init__(self):
- self.logger = logging.getLogger()
-
- def write(self, message):
- if message != '\n':
- self.logger.error(message)
-
- def flush(self):
- pass
-
-sys.stderr = ExceptionLogger()
-
-#----------------------------------------------------------
-# Drivers
-#----------------------------------------------------------
-
-drivers = []
-interfaces = {}
-iot_devices = {}
-
-class InterfaceMetaClass(type):
- def __new__(cls, clsname, bases, attrs):
- if clsname in interfaces:
- return interfaces[clsname]
- new_interface = super(InterfaceMetaClass, cls).__new__(cls, clsname, bases, attrs)
- interfaces[clsname] = new_interface
- return new_interface
-
-class Interface(Thread, metaclass=InterfaceMetaClass):
- _loop_delay = 3 # Delay (in seconds) between calls to get_devices or 0 if it should be called only once
- _detected_devices = {}
- connection_type = ''
-
- def __init__(self):
- super(Interface, self).__init__()
- self.drivers = sorted([d for d in drivers if d.connection_type == self.connection_type], key=lambda d: d.priority, reverse=True)
-
- def run(self):
- while self.connection_type and self.drivers:
- self.update_iot_devices(self.get_devices())
- if not self._loop_delay:
- break
- time.sleep(self._loop_delay)
-
- def update_iot_devices(self, devices={}):
- added = devices.keys() - self._detected_devices
- removed = self._detected_devices - devices.keys()
- self._detected_devices = devices.keys()
-
- for path in removed:
- if path in iot_devices:
- iot_devices[path].disconnect()
- _logger.info('Device %s is now disconnected', path)
-
- for path in added:
- for driver in self.drivers:
- if driver.supported(device=devices[path]):
- _logger.info('Device %s is now connected', path)
- d = driver(device=devices[path])
- d.daemon = True
- d.start()
- iot_devices[path] = d
- break
-
- def get_devices(self):
- raise NotImplementedError()
-
-class DriverMetaClass(type):
- def __new__(cls, clsname, bases, attrs):
- newclass = super(DriverMetaClass, cls).__new__(cls, clsname, bases, attrs)
- if hasattr(newclass, 'priority'):
- newclass.priority += 1
- else:
- newclass.priority = 0
- drivers.append(newclass)
- return newclass
-
-class Driver(Thread, metaclass=DriverMetaClass):
- """
- Hook to register the driver into the drivers list
- """
- connection_type = ""
-
- def __init__(self, device):
- super(Driver, self).__init__()
- self.dev = device
- self.data = {'value': ''}
- self._device_manufacturer = ''
-
- @property
- def device_name(self):
- return self._device_name
-
- @property
- def device_identifier(self):
- return self.dev.identifier
-
- @property
- def device_manufacturer(self):
- return self._device_manufacturer
-
- @property
- def device_connection(self):
- """
- On specific driver override this method to give connection type of device
- return string
- possible value : direct - network - bluetooth - serial - hdmi
- """
- return self._device_connection
-
- @property
- def device_type(self):
- """
- On specific driver override this method to give type of device
- return string
- possible value : printer - camera - keyboard - scanner - display - device
- """
- return self._device_type
-
- @classmethod
- def supported(cls, device):
- """
- On specific driver override this method to check if device is supported or not
- return True or False
- """
- pass
-
- def get_message(self):
- return ''
-
- def action(self, data):
- """
- On specific driver override this method to make a action with device (take picture, printing,...)
- """
- raise NotImplementedError()
-
- def disconnect(self):
- del iot_devices[self.device_identifier]
-
-
-#----------------------------------------------------------
-# Device manager
-#----------------------------------------------------------
-
-class EventManager(object):
- def __init__(self):
- self.sessions = {}
-
- def _delete_expired_sessions(self, max_time=70):
- '''
- Clears sessions that are no longer called.
-
- :param max_time: time a session can stay unused before being deleted
- '''
- now = time.time()
- expired_sessions = [session for session in self.sessions if now - self.sessions[session]['time_request'] > max_time]
- for session in expired_sessions:
- del self.sessions[session]
-
- def add_request(self, listener):
- self.session = {
- 'session_id': listener['session_id'],
- 'devices': listener['devices'],
- 'event': Event(),
- 'result': {},
- 'time_request': time.time(),
- }
- self._delete_expired_sessions()
- self.sessions[listener['session_id']] = self.session
- return self.sessions[listener['session_id']]
-
- def device_changed(self, device):
- for session in self.sessions:
- if device.device_identifier in self.sessions[session]['devices']:
- self.sessions[session]['result'] = device.data
- self.sessions[session]['result']['device_id'] = device.device_identifier
- self.sessions[session]['event'].set()
-
-
-event_manager = EventManager()
-
-#----------------------------------------------------------
-# ConnectionManager
-#----------------------------------------------------------
-
-class ConnectionManager(Thread):
- def __init__(self):
- super(ConnectionManager, self).__init__()
- self.pairing_code = False
- self.pairing_uuid = False
-
- def run(self):
- if not helpers.get_odoo_server_url():
- end_time = datetime.now() + timedelta(minutes=5)
- while (datetime.now() < end_time):
- self._connect_box()
- time.sleep(10)
- self.pairing_code = False
- self.pairing_uuid = False
- self._refresh_displays()
-
- def _connect_box(self):
- data = {
- 'jsonrpc': 2.0,
- 'params': {
- 'pairing_code': self.pairing_code,
- 'pairing_uuid': self.pairing_uuid,
- }
- }
-
- urllib3.disable_warnings()
- req = requests.post('https://iot-proxy.odoo.com/odoo-enterprise/iot/connect-box', json=data, verify=False)
- result = req.json().get('result', {})
-
- if all(key in result for key in ['pairing_code', 'pairing_uuid']):
- self.pairing_code = result['pairing_code']
- self.pairing_uuid = result['pairing_uuid']
- elif all(key in result for key in ['url', 'token', 'db_uuid', 'enterprise_code']):
- self._connect_to_server(result['url'], result['token'], result['db_uuid'], result['enterprise_code'])
-
- def _connect_to_server(self, url, token, db_uuid, enterprise_code):
- if db_uuid and enterprise_code:
- helpers.add_credential(db_uuid, enterprise_code)
-
- # Save DB URL and token
- subprocess.check_call([get_resource_path('point_of_sale', 'tools/posbox/configuration/connect_to_server.sh'), url, '', token, 'noreboot'])
- # Notify the DB, so that the kanban view already shows the IoT Box
- m.send_alldevices()
- # Restart to checkout the git branch, get a certificate, load the IoT handlers...
- subprocess.check_call(["sudo", "service", "odoo", "restart"])
-
- def _refresh_displays(self):
- """Refresh all displays to hide the pairing code"""
- for d in iot_devices:
- if iot_devices[d].device_type == 'display':
- iot_devices[d].action({
- 'action': 'display_refresh'
- })
-
-#----------------------------------------------------------
-# Manager
-#----------------------------------------------------------
-
-class Manager(Thread):
-
- def load_iot_handlers(self):
- """
- This method loads local files: 'odoo/addons/hw_drivers/iot_handlers/drivers' and
- 'odoo/addons/hw_drivers/iot_handlers/interfaces'
- And execute these python drivers and interfaces
- """
- helpers.download_iot_handlers()
- for directory in ['interfaces', 'drivers']:
- path = get_resource_path('hw_drivers', 'iot_handlers', directory)
- filesList = os.listdir(path)
- for file in filesList:
- path_file = os.path.join(path, file)
- spec = util.spec_from_file_location(file, path_file)
- if spec:
- module = util.module_from_spec(spec)
- spec.loader.exec_module(module)
- http.addons_manifest = {}
- http.root = http.Root()
-
- def send_alldevices(self):
- """
- This method send IoT Box and devices informations to Odoo database
- """
- server = helpers.get_odoo_server_url()
- if server:
- subject = helpers.read_file_first_line('odoo-subject.conf')
- if subject:
- domain = helpers.get_ip().replace('.', '-') + subject.strip('*')
- else:
- domain = helpers.get_ip()
- iot_box = {
- 'name': socket.gethostname(),
- 'identifier': helpers.get_mac_address(),
- 'ip': domain,
- 'token': helpers.get_token(),
- 'version': helpers.get_version()
- }
- devices_list = {}
- for device in iot_devices:
- identifier = iot_devices[device].device_identifier
- devices_list[identifier] = {
- 'name': iot_devices[device].device_name,
- 'type': iot_devices[device].device_type,
- 'manufacturer': iot_devices[device].device_manufacturer,
- 'connection': iot_devices[device].device_connection,
- }
- data = {
- 'params': {
- 'iot_box' : iot_box,
- 'devices' : devices_list,
- }
- }
- # disable certifiacte verification
- urllib3.disable_warnings()
- http = urllib3.PoolManager(cert_reqs='CERT_NONE')
- try:
- http.request(
- 'POST',
- server + "/iot/setup",
- body = json.dumps(data).encode('utf8'),
- headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
- )
- except Exception as e:
- _logger.error('Could not reach configured server')
- _logger.error('A error encountered : %s ' % e)
- else:
- _logger.warning('Odoo server not set')
-
- def run(self):
- """
- Thread that will check connected/disconnected device, load drivers if needed and contact the odoo server with the updates
- """
- helpers.check_git_branch()
- helpers.check_certificate()
- self.send_alldevices()
- self.load_iot_handlers()
- self.previous_iot_devices = []
- for interface in interfaces.values():
- i = interface()
- i.daemon = True
- i.start()
- while 1:
- if iot_devices != self.previous_iot_devices:
- self.send_alldevices()
- self.previous_iot_devices = iot_devices.copy()
- time.sleep(3)
-
-# Must be started from main thread
-DBusGMainLoop(set_as_default=True)
-
-cm = ConnectionManager()
-cm.daemon = True
-cm.start()
-
-m = Manager()
-m.daemon = True
-m.start()
diff --git a/addons/hw_drivers/controllers/proxy.py b/addons/hw_drivers/controllers/proxy.py
new file mode 100644
index 00000000000..b5124772d2a
--- /dev/null
+++ b/addons/hw_drivers/controllers/proxy.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import http
+
+proxy_drivers = {}
+
+class ProxyController(http.Controller):
+ @http.route('/hw_proxy/hello', type='http', auth='none', cors='*')
+ def hello(self):
+ return "ping"
+
+ @http.route('/hw_proxy/handshake', type='json', auth='none', cors='*')
+ def handshake(self):
+ return True
+
+ @http.route('/hw_proxy/status_json', type='json', auth='none', cors='*')
+ def status_json(self):
+ statuses = {}
+ for driver in proxy_drivers:
+ statuses[driver] = proxy_drivers[driver].get_status()
+ return statuses
diff --git a/addons/hw_drivers/driver.py b/addons/hw_drivers/driver.py
new file mode 100644
index 00000000000..bfe60effc6b
--- /dev/null
+++ b/addons/hw_drivers/driver.py
@@ -0,0 +1,51 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from threading import Thread
+
+from odoo.addons.hw_drivers.main import drivers, iot_devices
+
+
+class DriverMetaClass(type):
+ def __new__(cls, clsname, bases, attrs):
+ newclass = super(DriverMetaClass, cls).__new__(cls, clsname, bases, attrs)
+ if hasattr(newclass, 'priority'):
+ newclass.priority += 1
+ else:
+ newclass.priority = 0
+ drivers.append(newclass)
+ return newclass
+
+
+class Driver(Thread, metaclass=DriverMetaClass):
+ """
+ Hook to register the driver into the drivers list
+ """
+ connection_type = ''
+
+ def __init__(self, identifier, device):
+ super(Driver, self).__init__()
+ self.dev = device
+ self.device_identifier = identifier
+ self.device_name = ''
+ self.device_connection = ''
+ self.device_type = ''
+ self.device_manufacturer = ''
+ self.data = {'value': ''}
+
+ @classmethod
+ def supported(cls, device):
+ """
+ On specific driver override this method to check if device is supported or not
+ return True or False
+ """
+ return False
+
+ def action(self, data):
+ """
+ On specific driver override this method to make a action with device (take picture, printing,...)
+ """
+ raise NotImplementedError()
+
+ def disconnect(self):
+ del iot_devices[self.device_identifier]
diff --git a/addons/hw_drivers/event_manager.py b/addons/hw_drivers/event_manager.py
new file mode 100644
index 00000000000..59d30ea2318
--- /dev/null
+++ b/addons/hw_drivers/event_manager.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from threading import Event
+import time
+
+
+class EventManager(object):
+ def __init__(self):
+ self.sessions = {}
+
+ def _delete_expired_sessions(self, max_time=70):
+ '''
+ Clears sessions that are no longer called.
+
+ :param max_time: time a session can stay unused before being deleted
+ '''
+ now = time.time()
+ expired_sessions = [
+ session
+ for session in self.sessions
+ if now - self.sessions[session]['time_request'] > max_time
+ ]
+ for session in expired_sessions:
+ del self.sessions[session]
+
+ def add_request(self, listener):
+ self.session = {
+ 'session_id': listener['session_id'],
+ 'devices': listener['devices'],
+ 'event': Event(),
+ 'result': {},
+ 'time_request': time.time(),
+ }
+ self._delete_expired_sessions()
+ self.sessions[listener['session_id']] = self.session
+ return self.sessions[listener['session_id']]
+
+ def device_changed(self, device):
+ for session in self.sessions:
+ if device.device_identifier in self.sessions[session]['devices']:
+ self.sessions[session]['result'] = device.data
+ self.sessions[session]['result']['device_identifier'] = device.device_identifier
+ self.sessions[session]['event'].set()
+
+
+event_manager = EventManager()
diff --git a/addons/hw_drivers/exception_logger.py b/addons/hw_drivers/exception_logger.py
new file mode 100644
index 00000000000..0d1890b5f25
--- /dev/null
+++ b/addons/hw_drivers/exception_logger.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import logging
+import sys
+
+
+class ExceptionLogger:
+ """
+ Redirect Exceptions to the logger to keep track of them in the log file.
+ """
+
+ def __init__(self):
+ self.logger = logging.getLogger()
+
+ def write(self, message):
+ if message != '\n':
+ self.logger.error(message)
+
+ def flush(self):
+ pass
+
+sys.stderr = ExceptionLogger()
diff --git a/addons/hw_drivers/interface.py b/addons/hw_drivers/interface.py
new file mode 100644
index 00000000000..28ad1a55e0a
--- /dev/null
+++ b/addons/hw_drivers/interface.py
@@ -0,0 +1,59 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import logging
+from threading import Thread
+import time
+
+from odoo.addons.hw_drivers.main import drivers, interfaces, iot_devices
+
+_logger = logging.getLogger(__name__)
+
+
+class InterfaceMetaClass(type):
+ def __new__(cls, clsname, bases, attrs):
+ if clsname in interfaces:
+ return interfaces[clsname]
+ new_interface = super(InterfaceMetaClass, cls).__new__(cls, clsname, bases, attrs)
+ interfaces[clsname] = new_interface
+ return new_interface
+
+
+class Interface(Thread, metaclass=InterfaceMetaClass):
+ _loop_delay = 3 # Delay (in seconds) between calls to get_devices or 0 if it should be called only once
+ _detected_devices = {}
+ connection_type = ''
+
+ def __init__(self):
+ super(Interface, self).__init__()
+ self.drivers = sorted([d for d in drivers if d.connection_type == self.connection_type], key=lambda d: d.priority, reverse=True)
+
+ def run(self):
+ while self.connection_type and self.drivers:
+ self.update_iot_devices(self.get_devices())
+ if not self._loop_delay:
+ break
+ time.sleep(self._loop_delay)
+
+ def update_iot_devices(self, devices={}):
+ added = devices.keys() - self._detected_devices
+ removed = self._detected_devices - devices.keys()
+ self._detected_devices = devices.keys()
+
+ for identifier in removed:
+ if identifier in iot_devices:
+ iot_devices[identifier].disconnect()
+ _logger.info('Device %s is now disconnected', identifier)
+
+ for identifier in added:
+ for driver in self.drivers:
+ if driver.supported(devices[identifier]):
+ _logger.info('Device %s is now connected', identifier)
+ d = driver(identifier, devices[identifier])
+ d.daemon = True
+ d.start()
+ iot_devices[identifier] = d
+ break
+
+ def get_devices(self):
+ raise NotImplementedError()
diff --git a/addons/hw_drivers/iot_handlers/drivers/DisplayDriver.py b/addons/hw_drivers/iot_handlers/drivers/DisplayDriver.py
index b2076a58109..98854b98885 100644
--- a/addons/hw_drivers/iot_handlers/drivers/DisplayDriver.py
+++ b/addons/hw_drivers/iot_handlers/drivers/DisplayDriver.py
@@ -1,17 +1,22 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
import jinja2
import json
import logging
import netifaces as ni
import os
-from pathlib import Path
import subprocess
import threading
import time
import urllib3
from odoo import http
+from odoo.addons.hw_drivers.connection_manager import connection_manager
+from odoo.addons.hw_drivers.driver import Driver
+from odoo.addons.hw_drivers.event_manager import event_manager
+from odoo.addons.hw_drivers.main import iot_devices
from odoo.addons.hw_drivers.tools import helpers
-from odoo.addons.hw_drivers.controllers.driver import Driver, event_manager, iot_devices, cm
path = os.path.realpath(os.path.join(os.path.dirname(__file__), '../../views'))
loader = jinja2.FileSystemLoader(path)
@@ -27,11 +32,11 @@ _logger = logging.getLogger(__name__)
class DisplayDriver(Driver):
connection_type = 'display'
- def __init__(self, device):
- super(DisplayDriver, self).__init__(device)
- self._device_type = 'display'
- self._device_connection = 'hdmi'
- self._device_name = device['name']
+ def __init__(self, identifier, device):
+ super(DisplayDriver, self).__init__(identifier, device)
+ self.device_type = 'display'
+ self.device_connection = 'hdmi'
+ self.device_name = device['name']
self.event_data = threading.Event()
self.owner = False
self.rendered_html = ''
@@ -39,10 +44,6 @@ class DisplayDriver(Driver):
self._x_screen = device.get('x_screen', '0')
self.load_url()
- @property
- def device_identifier(self):
- return self.dev['identifier']
-
@classmethod
def supported(cls, device):
return True # All devices with connection_type == 'display' are supported
@@ -93,7 +94,7 @@ class DisplayDriver(Driver):
urllib3.disable_warnings()
http = urllib3.PoolManager(cert_reqs='CERT_NONE')
try:
- response = http.request('GET', "%s/iot/box/%s/screen_url" % (helpers.get_odoo_server_url(), helpers.get_mac_address()))
+ response = http.request('GET', "%s/iot/box/%s/display_url" % (helpers.get_odoo_server_url(), helpers.get_mac_address()))
if response.status == 200:
data = json.loads(response.data.decode('utf8'))
url = data[self.device_identifier]
@@ -215,5 +216,5 @@ class DisplayController(http.Controller):
'cust_js': cust_js,
'display_ifaces': display_ifaces,
'display_identifier': display_identifier,
- 'pairing_code': cm.pairing_code,
+ 'pairing_code': connection_manager.pairing_code,
})
diff --git a/addons/hw_drivers/iot_handlers/drivers/KeyboardUSBDriver.py b/addons/hw_drivers/iot_handlers/drivers/KeyboardUSBDriver.py
index 9425456ca9a..8c27f20b569 100644
--- a/addons/hw_drivers/iot_handlers/drivers/KeyboardUSBDriver.py
+++ b/addons/hw_drivers/iot_handlers/drivers/KeyboardUSBDriver.py
@@ -8,17 +8,19 @@ import logging
from lxml import etree
import os
from pathlib import Path
+from queue import Queue, Empty
import subprocess
-import time
from threading import Lock
-import usb
+import time
import urllib3
-from queue import Queue, Empty
+from usb import util
from odoo import http, _
-from odoo.addons.hw_proxy.controllers.main import drivers as old_drivers
+from odoo.addons.hw_drivers.controllers.proxy import proxy_drivers
+from odoo.addons.hw_drivers.driver import Driver
+from odoo.addons.hw_drivers.event_manager import event_manager
+from odoo.addons.hw_drivers.main import iot_devices
from odoo.addons.hw_drivers.tools import helpers
-from odoo.addons.hw_drivers.controllers.driver import event_manager, Driver, iot_devices
_logger = logging.getLogger(__name__)
xlib = ctypes.cdll.LoadLibrary('libX11.so.6')
@@ -29,14 +31,14 @@ class KeyboardUSBDriver(Driver):
keyboard_layout_groups = []
available_layouts = []
- def __init__(self, device):
+ def __init__(self, identifier, device):
if not hasattr(KeyboardUSBDriver, 'display'):
os.environ['XAUTHORITY'] = "/run/lightdm/pi/xauthority"
KeyboardUSBDriver.display = xlib.XOpenDisplay(bytes(":0.0", "utf-8"))
- super(KeyboardUSBDriver, self).__init__(device)
- self._device_connection = 'direct'
- self._device_name = self._set_name()
+ super(KeyboardUSBDriver, self).__init__(identifier, device)
+ self.device_connection = 'direct'
+ self.device_name = self._set_name()
# from https://github.com/xkbcommon/libxkbcommon/blob/master/test/evdev-scancodes.h
self._scancode_to_modifier = {
@@ -52,9 +54,9 @@ class KeyboardUSBDriver(Driver):
KeyboardUSBDriver.load_layouts_list()
KeyboardUSBDriver.send_layouts_list()
- for device in [evdev.InputDevice(path) for path in evdev.list_devices()]:
- if (self.dev.idVendor == device.info.vendor) and (self.dev.idProduct == device.info.product):
- self.input_device = device
+ for evdev_device in [evdev.InputDevice(path) for path in evdev.list_devices()]:
+ if (device.idVendor == evdev_device.info.vendor) and (device.idProduct == evdev_device.info.product):
+ self.input_device = evdev_device
self._set_device_type('scanner') if self._is_scanner() else self._set_device_type()
@@ -108,12 +110,8 @@ class KeyboardUSBDriver(Driver):
def _set_name(self):
try:
- if usb.__version__ == '1.0.0b1':
- manufacturer = usb.util.get_string(self.dev, 256, self.dev.iManufacturer)
- product = usb.util.get_string(self.dev, 256, self.dev.iProduct)
- else:
- manufacturer = usb.util.get_string(self.dev, self.dev.iManufacturer)
- product = usb.util.get_string(self.dev, self.dev.iProduct)
+ manufacturer = util.get_string(self.dev, self.dev.iManufacturer)
+ product = util.get_string(self.dev, self.dev.iProduct)
return ("%s - %s") % (manufacturer, product)
except ValueError as e:
_logger.warning(e)
@@ -134,7 +132,6 @@ class KeyboardUSBDriver(Driver):
self.data['value'] = ''
event_manager.device_changed(self)
-
def run(self):
try:
for event in self.input_device.read_loop():
@@ -228,7 +225,7 @@ class KeyboardUSBDriver(Driver):
"""Read the device type from the saved filed and set it as current type.
If no file or no device type is found we try to detect it automatically.
"""
- device_name = self._device_name.lower()
+ device_name = self.device_name.lower()
scanner_name = ['barcode', 'scanner', 'reader']
is_scanner = any(x in device_name for x in scanner_name) or self.dev.interface_protocol == '0'
@@ -276,14 +273,14 @@ class KeyboardUSBDriver(Driver):
type (string): Type wanted to switch
"""
if device_type == 'scanner':
- self._device_type = 'scanner'
+ self.device_type = 'scanner'
self.key_input = self._barcode_scanner_input
self._barcodes = Queue()
self._current_barcode = ''
self.input_device.grab()
self.read_barcode_lock = Lock()
else:
- self._device_type = 'keyboard'
+ self.device_type = 'keyboard'
self.key_input = self._keyboard_input
self.load_layout()
@@ -355,8 +352,8 @@ class KeyboardUSBDriver(Driver):
except Empty:
return ''
+proxy_drivers['scanner'] = KeyboardUSBDriver
-old_drivers['scanner'] = KeyboardUSBDriver
class KeyboardUSBController(http.Controller):
@http.route('/hw_proxy/scanner', type='json', auth='none', cors='*')
diff --git a/addons/hw_drivers/iot_handlers/drivers/PrinterDriver.py b/addons/hw_drivers/iot_handlers/drivers/PrinterDriver.py
index 7fd387f8a62..01041920583 100644
--- a/addons/hw_drivers/iot_handlers/drivers/PrinterDriver.py
+++ b/addons/hw_drivers/iot_handlers/drivers/PrinterDriver.py
@@ -4,21 +4,24 @@
from base64 import b64decode
from cups import IPPError, IPP_PRINTER_IDLE, IPP_PRINTER_PROCESSING, IPP_PRINTER_STOPPED
import dbus
+import io
import logging
import netifaces as ni
import os
-import io
-import base64
+from PIL import Image, ImageOps
import re
import subprocess
import tempfile
-from PIL import Image, ImageOps
+from uuid import getnode as get_mac
-from odoo import http, _
-from odoo.addons.hw_drivers.controllers.driver import event_manager, Driver, iot_devices, cm
+from odoo import http
+from odoo.addons.hw_drivers.connection_manager import connection_manager
+from odoo.addons.hw_drivers.controllers.proxy import proxy_drivers
+from odoo.addons.hw_drivers.driver import Driver
+from odoo.addons.hw_drivers.event_manager import event_manager
from odoo.addons.hw_drivers.iot_handlers.interfaces.PrinterInterface import PPDs, conn, cups_lock
+from odoo.addons.hw_drivers.main import iot_devices
from odoo.addons.hw_drivers.tools import helpers
-from odoo.addons.hw_proxy.controllers.main import drivers as old_drivers
_logger = logging.getLogger(__name__)
@@ -48,15 +51,15 @@ def print_star_error(deviceId):
process = subprocess.Popen(["lp", "-d", deviceId], stdin=subprocess.PIPE)
process.communicate(error_page.encode("utf-8"))
-def cups_notification_handler(message, uri, device_id, state, reason, accepting_jobs):
- if device_id in iot_devices:
+def cups_notification_handler(message, uri, device_identifier, state, reason, accepting_jobs):
+ if device_identifier in iot_devices:
reason = reason if reason != 'none' else None
state_value = {
IPP_PRINTER_IDLE: 'connected',
IPP_PRINTER_PROCESSING: 'processing',
IPP_PRINTER_STOPPED: 'stopped'
}
- iot_devices[device_id].update_status(state_value[state], message, reason)
+ iot_devices[device_identifier].update_status(state_value[state], message, reason)
# Create a Cups subscription if it doesn't exist yet
try:
@@ -76,18 +79,18 @@ bus.add_signal_receiver(cups_notification_handler, signal_name="PrinterStateChan
class PrinterDriver(Driver):
connection_type = 'printer'
- def __init__(self, device):
- super(PrinterDriver, self).__init__(device)
- self._device_type = 'printer'
- self._device_connection = self.dev['device-class'].lower()
- self._device_name = self.dev['device-make-and-model']
+ def __init__(self, identifier, device):
+ super(PrinterDriver, self).__init__(identifier, device)
+ self.device_type = 'printer'
+ self.device_connection = device['device-class'].lower()
+ self.device_name = device['device-make-and-model']
self.state = {
'status': 'connecting',
'message': 'Connecting to printer',
'reason': None,
}
self.send_status()
- if 'direct' in self._device_connection and 'CMD:ESC/POS;' in self.dev['device-id']:
+ if 'direct' in self.device_connection and 'CMD:ESC/POS;' in device['device-id']:
self.print_status()
@classmethod
@@ -137,15 +140,11 @@ class PrinterDriver(Driver):
status = 'connected' if any(iot_devices[d].device_type == "printer" and iot_devices[d].device_connection == 'direct' for d in iot_devices) else 'disconnected'
return {'status': status, 'messages': ''}
- @property
- def device_identifier(self):
- return self.dev['identifier']
-
def action(self, data):
if data.get('action') == 'cashbox':
self.open_cashbox()
elif data.get('action') == 'print_receipt':
- self.print_receipt(base64.b64decode(data['receipt']))
+ self.print_receipt(b64decode(data['receipt']))
else:
self.print_raw(b64decode(data['document']))
@@ -257,7 +256,7 @@ class PrinterDriver(Driver):
mac = '\nMAC Address:\n%s\n' % helpers.get_mac_address()
homepage = '\nHomepage:\nhttp://%s:8069\n\n' % main_ips
- code = cm.pairing_code
+ code = connection_manager.pairing_code
if code:
pairing_code = '\nPairing Code:\n%s\n' % code
@@ -286,4 +285,4 @@ class PrinterController(http.Controller):
return True
return False
-old_drivers['printer'] = PrinterDriver
+proxy_drivers['printer'] = PrinterDriver
diff --git a/addons/hw_drivers/iot_handlers/drivers/SerialBaseDriver.py b/addons/hw_drivers/iot_handlers/drivers/SerialBaseDriver.py
index 37a63f57548..6c3f1ceebb7 100644
--- a/addons/hw_drivers/iot_handlers/drivers/SerialBaseDriver.py
+++ b/addons/hw_drivers/iot_handlers/drivers/SerialBaseDriver.py
@@ -1,17 +1,17 @@
# -*- 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 logging
import serial
+from threading import Lock
+import time
+import traceback
-from odoo.tools.translate import _
-from odoo.addons.hw_drivers.controllers.driver import Driver, event_manager
+from odoo import _
+from odoo.addons.hw_drivers.event_manager import event_manager
+from odoo.addons.hw_drivers.driver import Driver
_logger = logging.getLogger(__name__)
@@ -58,26 +58,22 @@ class SerialDriver(Driver):
STATUS_ERROR = 'error'
STATUS_CONNECTING = 'connecting'
- def __init__(self, device):
+ def __init__(self, identifier, device):
""" Attributes initialization method for `SerialDriver`.
:param device: path to the device
:type device: str
"""
- super().__init__(device)
+ super(SerialDriver, self).__init__(identifier, device)
self._actions = {
'get_status': self._push_status,
}
- self._device_connection = 'serial'
+ self.device_connection = 'serial'
self._device_lock = Lock()
self._status = {'status': self.STATUS_CONNECTING, 'message_title': '', 'message_body': ''}
self._set_name()
- @property
- def device_identifier(self):
- return self.dev['identifier']
-
def _get_raw_response(connection):
pass
@@ -91,10 +87,10 @@ class SerialDriver(Driver):
"""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()
+ name = ('%s serial %s' % (self._protocol.name, self.device_type)).title()
except Exception:
name = 'Unknown Serial Device'
- self._device_name = name
+ self.device_name = name
def _take_measure(self):
pass
@@ -126,7 +122,7 @@ class SerialDriver(Driver):
if self._connection and self._connection.isOpen():
self._do_action(data)
else:
- with serial_connection(self.dev['identifier'], self._protocol) as connection:
+ with serial_connection(self.device_identifier, self._protocol) as connection:
self._connection = connection
self._do_action(data)
@@ -134,7 +130,7 @@ class SerialDriver(Driver):
"""Continuously gets new measures from the device."""
try:
- with serial_connection(self.dev['identifier'], self._protocol) as connection:
+ with serial_connection(self.device_identifier, self._protocol) as connection:
self._connection = connection
self._status['status'] = self.STATUS_CONNECTED
self._push_status()
diff --git a/addons/hw_drivers/iot_handlers/drivers/SerialScaleDriver.py b/addons/hw_drivers/iot_handlers/drivers/SerialScaleDriver.py
index 116beac799f..cc1d5469cad 100644
--- a/addons/hw_drivers/iot_handlers/drivers/SerialScaleDriver.py
+++ b/addons/hw_drivers/iot_handlers/drivers/SerialScaleDriver.py
@@ -1,18 +1,16 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-import threading
+from collections import namedtuple
import logging
import re
-import time
-from collections import namedtuple
-
import serial
+import threading
+import time
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.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
@@ -95,9 +93,9 @@ class ScaleDriver(SerialDriver):
"""Abstract base class for scale drivers."""
last_sent_value = None
- def __init__(self, device):
- self._device_type = 'scale'
- super().__init__(device)
+ def __init__(self, identifier, device):
+ super(ScaleDriver, self).__init__(identifier, device)
+ self.device_type = 'scale'
self._set_actions()
self._is_reading = True
@@ -105,7 +103,7 @@ class ScaleDriver(SerialDriver):
# Only the last scale connected is kept
global ACTIVE_SCALE
ACTIVE_SCALE = self
- old_drivers['scale'] = ACTIVE_SCALE
+ 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
@@ -212,9 +210,9 @@ class Toledo8217Driver(ScaleDriver):
"""Driver for the Toldedo 8217 serial scale."""
_protocol = Toledo8217Protocol
- def __init__(self, device):
- super().__init__(device)
- self._device_manufacturer = 'Toledo'
+ def __init__(self, identifier, device):
+ super(Toledo8217Driver, self).__init__(identifier, device)
+ self.device_manufacturer = 'Toledo'
@classmethod
def supported(cls, device):
@@ -249,11 +247,11 @@ class AdamEquipmentDriver(ScaleDriver):
_protocol = ADAMEquipmentProtocol
priority = 0 # Test the supported method of this driver last, after all other serial drivers
- def __init__(self, device):
- super().__init__(device)
+ def __init__(self, identifier, device):
+ super(AdamEquipmentDriver, self).__init__(identifier, device)
self._is_reading = False
self._last_weight_time = 0
- self._device_manufacturer = 'Adam'
+ 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":
diff --git a/addons/hw_drivers/iot_handlers/interfaces/DisplayInterface.py b/addons/hw_drivers/iot_handlers/interfaces/DisplayInterface.py
index 075da7a4e08..57e7a4afadc 100644
--- a/addons/hw_drivers/iot_handlers/interfaces/DisplayInterface.py
+++ b/addons/hw_drivers/iot_handlers/interfaces/DisplayInterface.py
@@ -1,7 +1,10 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
from re import sub, finditer
import subprocess
-from odoo.addons.hw_drivers.controllers.driver import Interface
+from odoo.addons.hw_drivers.interface import Interface
class DisplayInterface(Interface):
@@ -29,7 +32,6 @@ class DisplayInterface(Interface):
if not len(display_devices):
# No display connected, create "fake" device to be accessed from another computer
display_devices['distant_display'] = {
- 'identifier': "distant_display",
'name': "Distant Display",
}
diff --git a/addons/hw_drivers/iot_handlers/interfaces/PrinterInterface.py b/addons/hw_drivers/iot_handlers/interfaces/PrinterInterface.py
index e5d7dd89010..ca601dbce04 100644
--- a/addons/hw_drivers/iot_handlers/interfaces/PrinterInterface.py
+++ b/addons/hw_drivers/iot_handlers/interfaces/PrinterInterface.py
@@ -1,8 +1,11 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
from cups import Connection as cups_connection
from re import sub
from threading import Lock
-from odoo.addons.hw_drivers.controllers.driver import Interface
+from odoo.addons.hw_drivers.interface import Interface
conn = cups_connection()
PPDs = conn.getPPDs()
diff --git a/addons/hw_drivers/iot_handlers/interfaces/SerialInterface.py b/addons/hw_drivers/iot_handlers/interfaces/SerialInterface.py
index 82be2731a06..ae63969782f 100644
--- a/addons/hw_drivers/iot_handlers/interfaces/SerialInterface.py
+++ b/addons/hw_drivers/iot_handlers/interfaces/SerialInterface.py
@@ -1,6 +1,9 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
from glob import glob
-from odoo.addons.hw_drivers.controllers.driver import Interface
+from odoo.addons.hw_drivers.interface import Interface
class SerialInterface(Interface):
diff --git a/addons/hw_drivers/iot_handlers/interfaces/USBInterface.py b/addons/hw_drivers/iot_handlers/interfaces/USBInterface.py
index a1608189335..a40e82d6421 100644
--- a/addons/hw_drivers/iot_handlers/interfaces/USBInterface.py
+++ b/addons/hw_drivers/iot_handlers/interfaces/USBInterface.py
@@ -1,6 +1,9 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
from usb import core
-from odoo.addons.hw_drivers.controllers.driver import Interface
+from odoo.addons.hw_drivers.interface import Interface
class USBInterface(Interface):
@@ -18,9 +21,9 @@ class USBInterface(Interface):
devs = core.find(find_all=True)
cpt = 2
for dev in devs:
- dev.identifier = "usb_%04x:%04x" % (dev.idVendor, dev.idProduct)
- if dev.identifier in usb_devices:
- dev.identifier += '_%s' % cpt
+ identifier = "usb_%04x:%04x" % (dev.idVendor, dev.idProduct)
+ if identifier in usb_devices:
+ identifier += '_%s' % cpt
cpt += 1
- usb_devices[dev.identifier] = dev
+ usb_devices[identifier] = dev
return usb_devices
diff --git a/addons/hw_drivers/main.py b/addons/hw_drivers/main.py
new file mode 100644
index 00000000000..ccbbed52e92
--- /dev/null
+++ b/addons/hw_drivers/main.py
@@ -0,0 +1,104 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from dbus.mainloop.glib import DBusGMainLoop
+import json
+import logging
+import socket
+from threading import Thread
+import time
+import urllib3
+
+from odoo.addons.hw_drivers.tools import helpers
+
+_logger = logging.getLogger(__name__)
+
+drivers = []
+interfaces = {}
+iot_devices = {}
+
+
+class Manager(Thread):
+ def send_alldevices(self):
+ """
+ This method send IoT Box and devices informations to Odoo database
+ """
+ server = helpers.get_odoo_server_url()
+ if server:
+ subject = helpers.read_file_first_line('odoo-subject.conf')
+ if subject:
+ domain = helpers.get_ip().replace('.', '-') + subject.strip('*')
+ else:
+ domain = helpers.get_ip()
+ iot_box = {
+ 'name': socket.gethostname(),
+ 'identifier': helpers.get_mac_address(),
+ 'ip': domain,
+ 'token': helpers.get_token(),
+ 'version': helpers.get_version(),
+ }
+ devices_list = {}
+ for device in iot_devices:
+ identifier = iot_devices[device].device_identifier
+ devices_list[identifier] = {
+ 'name': iot_devices[device].device_name,
+ 'type': iot_devices[device].device_type,
+ 'manufacturer': iot_devices[device].device_manufacturer,
+ 'connection': iot_devices[device].device_connection,
+ }
+ data = {'params': {'iot_box': iot_box, 'devices': devices_list,}}
+ # disable certifiacte verification
+ urllib3.disable_warnings()
+ http = urllib3.PoolManager(cert_reqs='CERT_NONE')
+ try:
+ http.request(
+ 'POST',
+ server + "/iot/setup",
+ body=json.dumps(data).encode('utf8'),
+ headers={
+ 'Content-type': 'application/json',
+ 'Accept': 'text/plain',
+ },
+ )
+ except Exception as e:
+ _logger.error('Could not reach configured server')
+ _logger.error('A error encountered : %s ' % e)
+ else:
+ _logger.warning('Odoo server not set')
+
+ def run(self):
+ """
+ Thread that will load interfaces and drivers and contact the odoo server with the updates
+ """
+
+ helpers.check_git_branch()
+ helpers.check_certificate()
+
+ # We first add the IoT Box to the connected DB because IoT handlers cannot be downloaded if
+ # the identifier of the Box is not found in the DB. So add the Box to the DB.
+ self.send_alldevices()
+ helpers.download_iot_handlers()
+ helpers.load_iot_handlers()
+
+ # Start the interfaces
+ for interface in interfaces.values():
+ i = interface()
+ i.daemon = True
+ i.start()
+
+ # Check every 3 secondes if the list of connected devices has changed and send the updated
+ # list to the connected DB.
+ self.previous_iot_devices = []
+ while 1:
+ if iot_devices != self.previous_iot_devices:
+ self.send_alldevices()
+ self.previous_iot_devices = iot_devices.copy()
+ time.sleep(3)
+
+
+# Must be started from main thread
+DBusGMainLoop(set_as_default=True)
+
+manager = Manager()
+manager.daemon = True
+manager.start()
diff --git a/addons/hw_drivers/tools/helpers.py b/addons/hw_drivers/tools/helpers.py
index ad825e4e1cc..2c2a3b171a0 100644
--- a/addons/hw_drivers/tools/helpers.py
+++ b/addons/hw_drivers/tools/helpers.py
@@ -1,21 +1,22 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-import netifaces
-from pathlib import Path
import datetime
-from OpenSSL import crypto
-import urllib3
+from importlib import util
import io
import json
import logging
+import netifaces
+from OpenSSL import crypto
import os
+from pathlib import Path
import subprocess
+import urllib3
import zipfile
from threading import Thread
import time
-from odoo import _
+from odoo import _, http
from odoo.modules.module import get_resource_path
_logger = logging.getLogger(__name__)
@@ -232,6 +233,24 @@ def download_iot_handlers(auto=True):
_logger.error('Could not reach configured server')
_logger.error('A error encountered : %s ' % e)
+def load_iot_handlers():
+ """
+ This method loads local files: 'odoo/addons/hw_drivers/iot_handlers/drivers' and
+ 'odoo/addons/hw_drivers/iot_handlers/interfaces'
+ And execute these python drivers and interfaces
+ """
+ for directory in ['interfaces', 'drivers']:
+ path = get_resource_path('hw_drivers', 'iot_handlers', directory)
+ filesList = os.listdir(path)
+ for file in filesList:
+ path_file = os.path.join(path, file)
+ spec = util.spec_from_file_location(file, path_file)
+ if spec:
+ module = util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ http.addons_manifest = {}
+ http.root = http.Root()
+
def odoo_restart(delay):
IR = IoTRestart(delay)
IR.start()
diff --git a/addons/hw_escpos/__manifest__.py b/addons/hw_escpos/__manifest__.py
index dedb4b6bde9..de2e3d442d5 100644
--- a/addons/hw_escpos/__manifest__.py
+++ b/addons/hw_escpos/__manifest__.py
@@ -16,7 +16,6 @@ to open ESC/POS controlled cashdrawers in the point of sale and other modules
that would need such functionality.
""",
- 'depends': ['hw_proxy'],
'external_dependencies': {
'python' : ['pyusb','pyserial','qrcode'],
},
diff --git a/addons/hw_escpos/controllers/main.py b/addons/hw_escpos/controllers/main.py
index 160cee39342..123a8a41cec 100644
--- a/addons/hw_escpos/controllers/main.py
+++ b/addons/hw_escpos/controllers/main.py
@@ -27,7 +27,7 @@ except ImportError:
usb = None
from odoo import http, _
-from odoo.addons.hw_proxy.controllers import main as hw_proxy
+from odoo.addons.hw_drivers.controllers import proxy
_logger = logging.getLogger(__name__)
@@ -78,10 +78,7 @@ class EscposDriver(Thread):
for printer in printers:
try:
- if usb.__version__ == '1.0.0b1':
- description = usb.util.get_string(printer, 256, printer.iManufacturer) + " " + usb.util.get_string(printer, 256, printer.iProduct)
- else:
- description = usb.util.get_string(printer, printer.iManufacturer) + " " + usb.util.get_string(printer, printer.iProduct)
+ description = usb.util.get_string(printer, printer.iManufacturer) + " " + usb.util.get_string(printer, printer.iProduct)
except Exception as e:
_logger.error("Can not get printer description: %s" % e)
description = 'Unknown printer'
@@ -326,9 +323,10 @@ class EscposDriver(Thread):
driver = EscposDriver()
-hw_proxy.drivers['escpos'] = driver
+proxy.proxy_drivers['escpos'] = driver
+
-class EscposProxy(hw_proxy.Proxy):
+class EscposProxy(proxy.ProxyController):
@http.route('/hw_proxy/open_cashbox', type='json', auth='none', cors='*')
def open_cashbox(self):
diff --git a/addons/hw_posbox_homepage/__manifest__.py b/addons/hw_posbox_homepage/__manifest__.py
index e3117a519fe..83c63262445 100644
--- a/addons/hw_posbox_homepage/__manifest__.py
+++ b/addons/hw_posbox_homepage/__manifest__.py
@@ -19,6 +19,5 @@ If you activate this module, you won't be able to access the
regular Odoo interface anymore.
""",
- 'depends': ['hw_proxy'],
'installable': False,
}
diff --git a/addons/hw_posbox_homepage/controllers/main.py b/addons/hw_posbox_homepage/controllers/main.py
index ed050364988..138d0190ded 100644
--- a/addons/hw_posbox_homepage/controllers/main.py
+++ b/addons/hw_posbox_homepage/controllers/main.py
@@ -1,26 +1,23 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-import logging
import json
import jinja2
-import subprocess
-import socket
-import sys
-import netifaces
-import odoo
-from odoo import http
+import logging
import os
-from odoo.tools import misc
from pathlib import Path
+import socket
+import subprocess
+import sys
import threading
-from odoo.addons.hw_proxy.controllers import main as hw_proxy
-from odoo.addons.web.controllers import main as web
+from odoo import http
+from odoo.http import Response
from odoo.modules.module import get_resource_path
+
+from odoo.addons.hw_drivers.main import iot_devices
from odoo.addons.hw_drivers.tools import helpers
-from odoo.addons.hw_drivers.controllers.driver import iot_devices
-from odoo.http import Response
+from odoo.addons.web.controllers import main as web
_logger = logging.getLogger(__name__)
@@ -80,7 +77,7 @@ class IoTboxHomepage(web.Home):
iot_device.append({
'name': iot_devices[device].device_name + ' : ' + str(iot_devices[device].data['value']),
'type': iot_devices[device].device_type.replace('_', ' '),
- 'message': iot_devices[device].device_identifier + iot_devices[device].get_message()
+ 'identifier': iot_devices[device].device_identifier,
})
return {
diff --git a/addons/hw_posbox_homepage/views/homepage.html b/addons/hw_posbox_homepage/views/homepage.html
index 06f255d978e..482c5451489 100644
--- a/addons/hw_posbox_homepage/views/homepage.html
+++ b/addons/hw_posbox_homepage/views/homepage.html
@@ -27,7 +27,7 @@
.device-status {
margin: 6px 0;
}
- .device-status .message {
+ .device-status .identifier {
font-size: 0.8rem;
max-width: 350px;
}
@@ -130,7 +130,7 @@
{% for device in iot_devices.list %}
<div class="device-status">
<span class="device">{{ device['name'] }}</span>
- <div class="message">{{ device['message'] }}</div>
+ <div class="identifier">{{ device['identifier'] }}</div>
</div>
{% endfor %}
</div>
diff --git a/addons/hw_posbox_homepage/views/upgrade_page.html b/addons/hw_posbox_homepage/views/upgrade_page.html
index 0c87fc62d78..b9c2ae1d0e7 100644
--- a/addons/hw_posbox_homepage/views/upgrade_page.html
+++ b/addons/hw_posbox_homepage/views/upgrade_page.html
@@ -13,7 +13,7 @@
url:'/hw_proxy/perform_upgrade/'
}).done(function() {
$('.message-title').text('Upgrade successful');
- var cpt = 110;
+ var cpt = 25;
setInterval(function() {
--cpt;
if (cpt === 0) {location.reload();}
diff --git a/addons/hw_proxy/__init__.py b/addons/hw_proxy/__init__.py
deleted file mode 100644
index dd15b5c32e5..00000000000
--- a/addons/hw_proxy/__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_proxy/__manifest__.py b/addons/hw_proxy/__manifest__.py
deleted file mode 100644
index 9250ced17d3..00000000000
--- a/addons/hw_proxy/__manifest__.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# -*- coding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-{
- 'name': 'Hardware Proxy',
- 'category': 'Sales/Point of Sale',
- 'sequence': 6,
- 'summary': 'Connect the Web Client to Hardware Peripherals',
- 'website': 'https://www.odoo.com/page/point-of-sale-hardware',
- 'description': """
-Hardware Poxy
-=============
-
-This module allows you to remotely use peripherals connected to this server.
-
-This modules only contains the enabling framework. The actual devices drivers
-are found in other modules that must be installed separately.
-
-""",
- 'installable': False,
-}
diff --git a/addons/hw_proxy/controllers/__init__.py b/addons/hw_proxy/controllers/__init__.py
deleted file mode 100644
index 5d4b25db9c0..00000000000
--- a/addons/hw_proxy/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_proxy/controllers/main.py b/addons/hw_proxy/controllers/main.py
deleted file mode 100644
index 797644a395d..00000000000
--- a/addons/hw_proxy/controllers/main.py
+++ /dev/null
@@ -1,218 +0,0 @@
-# -*- coding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-from __future__ import print_function
-import logging
-import subprocess
-import time
-import subprocess
-from threading import Lock
-
-
-from odoo import http
-from odoo.http import request
-
-_logger = logging.getLogger(__name__)
-
-
-# Those are the builtin raspberry pi USB modules, they should
-# not appear in the list of connected devices.
-BANNED_DEVICES = {
- "0424:9514", # Standard Microsystem Corp. Builtin Ethernet module
- "1d6b:0002", # Linux Foundation 2.0 root hub
- "0424:ec00", # Standard Microsystem Corp. Other Builtin Ethernet module
- "0424:2514", # Standard Microsystems Corp. USB 2.0 Hub (rpi3b+)
- "0424:7800", # Standard Microsystems Corp. (rpi3b+)
-}
-
-
-# drivers modules must add to drivers an object with a get_status() method
-# so that 'status' can return the status of all active drivers
-drivers = {}
-
-# keep a list of RS-232 devices that have been recognized by a driver,
-# so other drivers can skip them during probes
-rs232_devices = {} # {'/path/to/device': 'driver'}
-rs232_lock = Lock() # must be held to update `rs232_devices`
-
-class Proxy(http.Controller):
-
- def get_status(self):
- statuses = {}
- for driver in drivers:
- statuses[driver] = drivers[driver].get_status()
- return statuses
-
- @http.route('/hw_proxy/hello', type='http', auth='none', cors='*')
- def hello(self):
- return "ping"
-
- @http.route('/hw_proxy/handshake', type='json', auth='none', cors='*')
- def handshake(self):
- return True
-
- @http.route('/hw_proxy/status', type='http', auth='none', cors='*')
- def status_http(self, debug=None, **kwargs):
- resp = """
-<!DOCTYPE HTML>
-<html>
- <head>
- <title>Odoo's IoTBox</title>
- <style>
- body {
- width: 480px;
- margin: 60px auto;
- font-family: sans-serif;
- text-align: justify;
- color: #6B6B6B;
- }
- .device {
- border-bottom: solid 1px rgb(216,216,216);
- padding: 9px;
- }
- .device:nth-child(2n) {
- background:rgb(240,240,240);
- }
- </style>
- </head>
- <body>
- <h1>Hardware Status</h1>
- <p>The list of enabled drivers and their status</p>
-"""
- statuses = self.get_status()
- for driver in statuses:
-
- status = statuses[driver]
-
- if status['status'] == 'connecting':
- color = 'black'
- elif status['status'] == 'connected':
- color = 'green'
- else:
- color = 'red'
-
- resp += "<h3 style='color:"+color+";'>"+driver+' : '+status['status']+"</h3>\n"
- resp += "<ul>\n"
- for msg in status['messages']:
- resp += '<li>'+msg+'</li>\n'
- resp += "</ul>\n"
- resp += """
- <h2>Connected Devices</h2>
- <p>The list of connected USB devices as seen by the IoTBox</p>
- """
- if debug is None:
- resp += """(<a href="/hw_proxy/status?debug=1">debug version</a>)"""
- devices = subprocess.check_output("lsusb").decode('utf-8').split('\n')
- count = 0
- resp += "<div class='devices'>\n"
- for device in devices:
- device_name = device[device.find('ID')+2:]
- if device_name: # to avoid last empty line
- device_id = device_name.split()[0]
- if not (device_id in BANNED_DEVICES):
- resp += "<div class='device' data-device='"+device+"'>"+device_name+"</div>\n"
- count += 1
-
- if count == 0:
- resp += "<div class='device'>No USB Device Found</div>"
-
- resp += "</div>\n</body>\n</html>\n\n"
-
- if debug is not None:
- resp += """
-
- <h3>Debug version</h3>
- <p><tt>lsusb -v</tt> output:</p>
- <pre>
- %s
- </pre>
-
- """ % subprocess.check_output(['lsusb', '-v']).decode('utf-8')
-
- return request.make_response(resp,{
- 'Cache-Control': 'no-cache',
- 'Content-Type': 'text/html; charset=utf-8',
- 'Access-Control-Allow-Origin': '*',
- 'Access-Control-Allow-Methods': 'GET',
- })
-
- @http.route('/hw_proxy/status_json', type='json', auth='none', cors='*')
- def status_json(self):
- return self.get_status()
-
- @http.route('/hw_proxy/scan_item_success', type='json', auth='none', cors='*')
- def scan_item_success(self, ean):
- """
- A product has been scanned with success
- """
- print('scan_item_success: %s', ean)
-
- @http.route('/hw_proxy/scan_item_error_unrecognized', type='json', auth='none', cors='*')
- def scan_item_error_unrecognized(self, ean):
- """
- A product has been scanned without success
- """
- print('scan_item_error_unrecognized: %s', ean)
-
- @http.route('/hw_proxy/help_needed', type='json', auth='none', cors='*')
- def help_needed(self):
- """
- The user wants an help (ex: light is on)
- """
- print("help_needed")
-
- @http.route('/hw_proxy/help_canceled', type='json', auth='none', cors='*')
- def help_canceled(self):
- """
- The user stops the help request
- """
- print("help_canceled")
-
- @http.route('/hw_proxy/payment_request', type='json', auth='none', cors='*')
- def payment_request(self, price):
- """
- The PoS will activate the method payment
- """
- print("payment_request: price:%s" % price)
- return 'ok'
-
- @http.route('/hw_proxy/payment_status', type='json', auth='none', cors='*')
- def payment_status(self):
- print("payment_status")
- return { 'status':'waiting' }
-
- @http.route('/hw_proxy/payment_cancel', type='json', auth='none', cors='*')
- def payment_cancel(self):
- print("payment_cancel")
-
- @http.route('/hw_proxy/transaction_start', type='json', auth='none', cors='*')
- def transaction_start(self):
- print('transaction_start')
-
- @http.route('/hw_proxy/transaction_end', type='json', auth='none', cors='*')
- def transaction_end(self):
- print('transaction_end')
-
- @http.route('/hw_proxy/cashier_mode_activated', type='json', auth='none', cors='*')
- def cashier_mode_activated(self):
- print('cashier_mode_activated')
-
- @http.route('/hw_proxy/cashier_mode_deactivated', type='json', auth='none', cors='*')
- def cashier_mode_deactivated(self):
- print('cashier_mode_deactivated')
-
- @http.route('/hw_proxy/open_cashbox', type='json', auth='none', cors='*')
- def open_cashbox(self):
- print('open_cashbox')
-
- @http.route('/hw_proxy/print_receipt', type='json', auth='none', cors='*')
- def print_receipt(self, receipt):
- print('print_receipt %s', receipt)
-
- @http.route('/hw_proxy/log', type='json', auth='none', cors='*')
- def log(self, arguments):
- _logger.info(' '.join(str(v) for v in arguments))
-
- @http.route('/hw_proxy/print_pdf_invoice', type='json', auth='none', cors='*')
- def print_pdf_invoice(self, pdfinvoice):
- print('print_pdf_invoice %s' % pdfinvoice)
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 8b346ec8dcb..8c9559a477c 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
@@ -31,7 +31,7 @@ function _start() {
while ! test -a /run/systemd/timesync/synchronized && ! ip route | grep 10.11.12.1 && ((cnt--)); do
sleep 1
done
- 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_escpos,hw_drivers --limit-time-cpu=600 --limit-time-real=1200
+ start-stop-daemon --start --quiet --pidfile $PIDFILE --chuid $USER:$USER --background --make-pidfile --exec $DAEMON -- --config $CONFIG --logfile $LOGFILE --load=web,hw_posbox_homepage,hw_escpos,hw_drivers --limit-time-cpu=600 --limit-time-real=1200
}
function _stop() {
diff --git a/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init_posbox_image.sh b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init_posbox_image.sh
index 5a961b032e5..f5af6ee7ee5 100755
--- a/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init_posbox_image.sh
+++ b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init_posbox_image.sh
@@ -27,77 +27,65 @@ apt-get update && apt-get -y upgrade
# Firmware 4.44 seems to prevent the LED mechanism from working
PKGS_TO_INSTALL="
- fswebcam \
- nginx-full \
- dnsmasq \
- dbus \
- dbus-x11 \
+ console-data \
cups \
- printer-driver-all \
cups-ipp-utils \
- libcups2-dev \
- pcscd \
- localepurge \
- vim \
- mc \
- mg \
- screen \
- iw \
- hostapd \
+ dbus \
+ dbus-x11 \
+ dnsmasq \
+ firefox-esr \
+ fswebcam \
git \
- rsync \
+ hostapd \
+ iw \
kpartx \
- swig \
- console-data \
+ libcups2-dev \
+ libpq-dev \
lightdm \
- xserver-xorg-video-fbdev \
- xserver-xorg-input-evdev \
- firefox-esr \
- xdotool \
- unclutter \
- x11-utils \
- xserver-xorg-video-dummy \
+ localepurge \
+ nginx-full \
openbox \
- rpi-update \
- adduser \
- libpq-dev \
+ printer-driver-all \
python-cups \
python3 \
- python3-pyscard \
- python3-urllib3 \
+ python3-babel \
python3-dateutil \
python3-decorator \
+ python3-dev \
python3-docutils \
python3-feedparser \
- python3-pil \
+ python3-html2text \
python3-jinja2 \
python3-ldap \
+ python3-libsass \
python3-lxml \
python3-mako \
python3-mock \
- python3-openid \
+ python3-netifaces \
+ python3-passlib \
+ python3-pil \
+ python3-pip \
python3-psutil \
python3-psycopg2 \
- python3-babel \
python3-pydot \
python3-pyparsing \
python3-pypdf2 \
+ python3-qrcode \
python3-reportlab \
python3-requests \
- python3-simplejson \
- python3-stdnum \
+ python3-serial \
python3-tz \
+ python3-urllib3 \
python3-werkzeug \
- python3-serial \
- python3-pip \
- python3-dev \
- python3-netifaces \
- python3-passlib \
- python3-libsass \
- python3-qrcode \
- python3-html2text \
- python3-unittest2 \
- python3-simplejson"
+ rsync \
+ screen \
+ unclutter \
+ vim \
+ x11-utils \
+ xdotool \
+ xserver-xorg-input-evdev \
+ xserver-xorg-video-dummy \
+ xserver-xorg-video-fbdev"
echo "Acquire::Retries "16";" > /etc/apt/apt.conf.d/99acquire-retries
# KEEP OWN CONFIG FILES DURING PACKAGE CONFIGURATION
@@ -114,12 +102,12 @@ rm -rfv /usr/share/doc
# so we install the latest pyusb that works with this libusb.
# Even in stretch, we had an error with langid (but worked otherwise)
PIP_TO_INSTALL="
- pyusb==1.0.0b1 \
evdev \
gatt \
- v4l2 \
polib \
- pycups"
+ pycups \
+ pyusb \
+ v4l2"
pip3 install ${PIP_TO_INSTALL}