From f03026a3d74581722dbc46ce96034f08a14d0c35 Mon Sep 17 00:00:00 2001 From: Antoine Prieels Date: Tue, 13 Aug 2019 14:13:27 +0000 Subject: [PATCH] [REF] hw_{drivers,screen}: Convert hw_screen into an IoT Driver Move all the logic that was inside hw_screen into an IoT Driver in order to have consistency between devices. TaskID: 1933834 --- addons/hw_drivers/controllers/driver.py | 27 ++- addons/hw_drivers/drivers/DisplayDriver.py | 173 ++++++++++++++++++ .../static/src/js/worker.js | 16 +- .../views/pos_display.html | 0 addons/hw_screen/__init__.py | 4 - addons/hw_screen/__manifest__.py | 21 --- addons/hw_screen/controllers/__init__.py | 4 - addons/hw_screen/controllers/main.py | 144 --------------- .../overwrite_before_init/etc/init.d/odoo | 2 +- 9 files changed, 204 insertions(+), 187 deletions(-) create mode 100644 addons/hw_drivers/drivers/DisplayDriver.py rename addons/{hw_screen => hw_drivers}/static/src/js/worker.js (85%) rename addons/{hw_screen => hw_drivers}/views/pos_display.html (100%) delete mode 100644 addons/hw_screen/__init__.py delete mode 100644 addons/hw_screen/__manifest__.py delete mode 100644 addons/hw_screen/controllers/__init__.py delete mode 100644 addons/hw_screen/controllers/main.py diff --git a/addons/hw_drivers/controllers/driver.py b/addons/hw_drivers/controllers/driver.py index 7cda51f9f9d..5b01df584f4 100755 --- a/addons/hw_drivers/controllers/driver.py +++ b/addons/hw_drivers/controllers/driver.py @@ -142,7 +142,7 @@ class Driver(Thread, metaclass=DriverMetaClass): """ On specific driver override this method to give connection type of device return string - possible value : direct - network - bluetooth - serial + possible value : direct - network - bluetooth - serial - hdmi """ return self._device_connection @@ -151,7 +151,7 @@ class Driver(Thread, metaclass=DriverMetaClass): """ On specific driver override this method to give type of device return string - possible value : printer - camera - keyboard - scanner - device + possible value : printer - camera - keyboard - scanner - display - device """ return self._device_type @@ -294,6 +294,26 @@ class Manager(Thread): else: _logger.warning('Odoo server not set') + def get_connected_displays(self): + display_devices = {} + hdmi = subprocess.check_output(['tvservice', '-n']).decode('utf-8') + if hdmi.find('=') != -1: + hdmi_serial = sub('[^a-zA-Z0-9 ]+', '', hdmi.split('=')[1]).replace(' ', '_') + iot_device = IoTDevice({ + 'identifier': hdmi_serial, + 'name': hdmi.split('=')[1], + }, 'display') + display_devices[hdmi_serial] = iot_device + + if not len(display_devices): + # No display connected, create "fake" device to be accessed from another computer + display_devices['distant_display'] = IoTDevice({ + 'identifier': "distant_display", + 'name': "Distant Display", + }, 'display') + + return display_devices + def serial_loop(self): serial_devices = {} for identifier in glob('/dev/serial/by-path/*'): @@ -364,11 +384,14 @@ class Manager(Thread): updated_devices = {} self.send_alldevices() self.load_drivers() + # The list of devices doesn't change after the Raspberry has booted + display_devices = self.get_connected_displays() cpt = 0 while 1: updated_devices = self.usb_loop() updated_devices.update(self.video_loop()) updated_devices.update(mpdm.devices) + updated_devices.update(display_devices) updated_devices.update(bt_devices) updated_devices.update(socket_devices) updated_devices.update(self.serial_loop()) diff --git a/addons/hw_drivers/drivers/DisplayDriver.py b/addons/hw_drivers/drivers/DisplayDriver.py new file mode 100644 index 00000000000..a80a7cf8f4d --- /dev/null +++ b/addons/hw_drivers/drivers/DisplayDriver.py @@ -0,0 +1,173 @@ +import jinja2 +import json +import logging +import netifaces as ni +import os +import subprocess +import threading +import time + +from odoo import http +from odoo.addons.hw_drivers.controllers.driver import Driver, event_manager, iot_devices, get_odoo_server_url, get_mac_address + +path = os.path.realpath(os.path.join(os.path.dirname(__file__), '../views')) +loader = jinja2.FileSystemLoader(path) + +jinja_env = jinja2.Environment(loader=loader, autoescape=True) +jinja_env.filters["json"] = json.dumps + +pos_display_template = jinja_env.get_template('pos_display.html') + +_logger = logging.getLogger(__name__) + +# Get screen_url from the Box for backward compatibility +box_url = False +if get_odoo_server_url(): + result = requests.get("%s/iot/box/%s/screen_url" % (get_odoo_server_url(), get_mac_address()), {}) + if result.status_code == 200: + box_url = result.text + + +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'] + self.event_data = threading.Event() + self.owner = False + self.rendered_html = '' + self.url = 'http://localhost:8069/point_of_sale/display/' + + @property + def device_identifier(self): + return self.dev['identifier'] + + @classmethod + def supported(cls, device): + return True # All devices with connection_type == 'display' are supported + + @classmethod + def get_default_display(cls): + displays = list(filter(lambda d: iot_devices[d].device_type == 'display', iot_devices)) + return len(displays) and iot_devices[displays[0]] + + def action(self, data): + if data.get('action') == "display_refresh": + self.call_xdotools('F5') + elif data.get('action') == "take_control": + self.take_control(self.data['owner'], data.get('html')) + elif data.get('action') == "customer_facing_display": + self.update_customer_facing_display(self.data['owner'], data.get('html')) + elif data.get('action') == "get_owner": + self.data = { + 'value': '', + 'owner': self.owner, + } + event_manager.device_changed(self) + + def call_xdotools(self, keystroke): + os.environ['DISPLAY'] = ":0.0" + os.environ['XAUTHORITY'] = "/run/lightdm/pi/xauthority" + try: + subprocess.call(['xdotool', 'key', keystroke]) + return "xdotool succeeded in stroking " + keystroke + except: + return "xdotool threw an error, maybe it is not installed on the IoTBox" + + def update_customer_facing_display(self, origin, html=None): + if origin == self.owner: + self.rendered_html = html + self.event_data.set() + + def get_serialized_order(self): + # IMPLEMENTATION OF LONGPOLLING + # Times out 2 seconds before the JS request does + if self.event_data.wait(28): + self.event_data.clear() + return {'rendered_html': self.rendered_html} + return {'rendered_html': False} + + def take_control(self, new_owner, html=None): + # ALLOW A CASHIER TO TAKE CONTROL OVER THE POSBOX, IN CASE OF MULTIPLE CASHIER PER DISPLAY + self.owner = new_owner + self.rendered_html = html + self.data = { + 'value': '', + 'owner': self.owner, + } + event_manager.device_changed(self) + self.event_data.set() + +class DisplayController(http.Controller): + + @http.route('/hw_proxy/display_refresh', type='json', auth='none', cors='*') + def display_refresh(self): + display = DisplayDriver.get_default_display() + if display: + return display.call_xdotools('F5') + + @http.route('/hw_proxy/customer_facing_display', type='json', auth='none', cors='*') + def customer_facing_display(self, html=None): + display = DisplayDriver.get_default_display() + if display: + display.update_customer_facing_display(http.request.httprequest.remote_addr, html) + return {'status': 'updated'} + return {'status': 'failed'} + + @http.route('/hw_proxy/take_control', type='json', auth='none', cors='*') + def take_control(self, html=None): + display = DisplayDriver.get_default_display() + if display: + display.take_control(http.request.httprequest.remote_addr, html) + return { + 'status': 'success', + 'message': 'You now have access to the display', + } + + @http.route('/hw_proxy/test_ownership', type='json', auth='none', cors='*') + def test_ownership(self): + display = DisplayDriver.get_default_display() + if display and display.owner == http.request.httprequest.remote_addr: + return {'status': 'OWNER'} + return {'status': 'NOWNER'} + + @http.route(['/point_of_sale/get_serialized_order'], type='json', auth='none') + def get_serialized_order(self): + display = DisplayDriver.get_default_display() + if display: + return display.get_serialized_order() + return { + 'rendered_html': False, + 'error': "No display found", + } + + @http.route(['/point_of_sale/display'], type='http', auth='none') + def display(self): + cust_js = None + interfaces = ni.interfaces() + + with open(os.path.join(os.path.dirname(__file__), "../static/src/js/worker.js")) as js: + cust_js = js.read() + + display_ifaces = [] + for iface_id in interfaces: + if 'wlan' in iface_id or 'eth' in iface_id: + iface_obj = ni.ifaddresses(iface_id) + ifconfigs = iface_obj.get(ni.AF_INET, []) + for conf in ifconfigs: + if conf.get('addr'): + display_ifaces.append({ + 'iface_id': iface_id, + 'addr': conf.get('addr'), + 'icon': 'sitemap' if 'eth' in iface_id else 'wifi', + }) + + return pos_display_template.render({ + 'title': "Odoo -- Point of Sale", + 'breadcrumb': 'POS Client display', + 'cust_js': cust_js, + 'display_ifaces': display_ifaces, + }) diff --git a/addons/hw_screen/static/src/js/worker.js b/addons/hw_drivers/static/src/js/worker.js similarity index 85% rename from addons/hw_screen/static/src/js/worker.js rename to addons/hw_drivers/static/src/js/worker.js index 5949ab39cef..a485a946588 100644 --- a/addons/hw_screen/static/src/js/worker.js +++ b/addons/hw_drivers/static/src/js/worker.js @@ -4,7 +4,6 @@ // It allows to transform the only once var mergedHead = false; var current_client_url = ""; - var stop_longpolling = false; function longpolling() { $.ajax({ @@ -15,14 +14,13 @@ data: JSON.stringify({jsonrpc: '2.0'}), success: function(data) { - if (typeof data.result.stop_longpolling !== 'undefined') { - stop_longpolling = data.result.stop_longpolling; - } if (data.result.error) { $('.error-message').text(data.result.error); $('.error-message').removeClass('d-none'); + setTimeout(longpolling, 5000); + return; } - if (data.result.ip_from && data.result.rendered_html) { + if (data.result.rendered_html) { var trimmed = $.trim(data.result.rendered_html); var $parsedHTML = $('
').html($.parseHTML(trimmed,true)); // WARNING: the true here will executes any script present in the string to parse var new_client_url = $parsedHTML.find(".resources > base").attr('href'); @@ -47,15 +45,11 @@ foreign_js(); } } - if (!stop_longpolling) { - longpolling(); - } + longpolling(); }, error: function (jqXHR, status, err) { - if (!stop_longpolling) { - setTimeout(longpolling, 5000); - } + setTimeout(longpolling, 5000); }, timeout: 30000, diff --git a/addons/hw_screen/views/pos_display.html b/addons/hw_drivers/views/pos_display.html similarity index 100% rename from addons/hw_screen/views/pos_display.html rename to addons/hw_drivers/views/pos_display.html diff --git a/addons/hw_screen/__init__.py b/addons/hw_screen/__init__.py deleted file mode 100644 index dd15b5c32e5..00000000000 --- a/addons/hw_screen/__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_screen/__manifest__.py b/addons/hw_screen/__manifest__.py deleted file mode 100644 index 756d385a550..00000000000 --- a/addons/hw_screen/__manifest__.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. -{ - 'name': 'Screen Driver', - 'version': '1.0', - 'category': 'Hardware Drivers', - 'sequence': 6, - 'summary': 'Add a customer facing displays for POS', - 'website': 'https://www.odoo.com/page/point-of-sale-hardware', - 'description': """ -Screen Driver -============= - -This module allows the POS client to send rendered HTML to a remotely -installed screen. This module then displays this HTML using a web -browser. -""", - 'depends': ['hw_proxy'], - 'installable': False, - 'auto_install': False, -} diff --git a/addons/hw_screen/controllers/__init__.py b/addons/hw_screen/controllers/__init__.py deleted file mode 100644 index 5d4b25db9c0..00000000000 --- a/addons/hw_screen/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_screen/controllers/main.py b/addons/hw_screen/controllers/main.py deleted file mode 100644 index 3579a8b0db7..00000000000 --- a/addons/hw_screen/controllers/main.py +++ /dev/null @@ -1,144 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import http -from odoo.tools import config -from odoo.addons.web.controllers import main as web - -import jinja2 -import json -import logging -import netifaces as ni -import os -from subprocess import call -import sys -import time -import threading - -self_port = str(config['http_port'] or 8069) - -_logger = logging.getLogger(__name__) - -if hasattr(sys, 'frozen'): - # When running on compiled windows binary, we don't have access to package loader. - path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', 'views')) - loader = jinja2.FileSystemLoader(path) -else: - loader = jinja2.PackageLoader('odoo.addons.hw_screen', "views") - -jinja_env = jinja2.Environment(loader=loader, autoescape=True) -jinja_env.filters["json"] = json.dumps - -pos_display_template = jinja_env.get_template('pos_display.html') - - -class HardwareScreen(web.Home): - - event_data = threading.Event() - pos_client_data = {'rendered_html': False, - 'ip_from': False} - display_in_use = '' - failure_count = {} - - def _call_xdotools(self, keystroke): - os.environ['DISPLAY'] = ":0.0" - os.environ['XAUTHORITY'] = "/run/lightdm/pi/xauthority" - try: - call(['xdotool', 'key', keystroke]) - return "xdotool succeeded in stroking " + keystroke - except: - return "xdotool threw an error, maybe it is not installed on the IoTBox" - - @http.route('/hw_proxy/display_refresh', type='json', auth='none', cors='*') - def display_refresh(self): - return self._call_xdotools('F5') - - # POS CASHIER'S ROUTES - @http.route('/hw_proxy/customer_facing_display', type='json', auth='none', cors='*') - def update_user_facing_display(self, html=None): - request_ip = http.request.httprequest.remote_addr - if request_ip == HardwareScreen.pos_client_data.get('ip_from', ''): - HardwareScreen.pos_client_data['rendered_html'] = html - HardwareScreen.event_data.set() - - return {'status': 'updated'} - else: - return {'status': 'failed'} - - @http.route('/hw_proxy/take_control', type='json', auth='none', cors='*') - def take_control(self, html=None): - # ALLOW A CASHIER TO TAKE CONTROL OVER THE POSBOX, IN CASE OF MULTIPLE CASHIER PER POSBOX - HardwareScreen.pos_client_data['rendered_html'] = html - HardwareScreen.pos_client_data['ip_from'] = http.request.httprequest.remote_addr - HardwareScreen.event_data.set() - - return {'status': 'success', - 'message': 'You now have access to the display'} - - @http.route('/hw_proxy/test_ownership', type='json', auth='none', cors='*') - def test_ownership(self): - if HardwareScreen.pos_client_data.get('ip_from') == http.request.httprequest.remote_addr: - return {'status': 'OWNER'} - else: - return {'status': 'NOWNER'} - - # POSBOX ROUTES (SELF) - @http.route('/point_of_sale/display', type='http', auth='none') - def render_main_display(self): - return self._get_html() - - @http.route('/point_of_sale/get_serialized_order', type='json', auth='none') - def get_serialized_order(self): - request_addr = http.request.httprequest.remote_addr - result = HardwareScreen.pos_client_data - if HardwareScreen.display_in_use and request_addr != HardwareScreen.display_in_use: - if not HardwareScreen.failure_count.get(request_addr): - HardwareScreen.failure_count[request_addr] = 0 - if HardwareScreen.failure_count[request_addr] > 0: - time.sleep(10) - HardwareScreen.failure_count[request_addr] += 1 - return { - 'rendered_html': False, - 'error': "Not Authorized. Another browser is in use to display for the client. Please refresh.", - 'stop_longpolling': True, - 'ip_from': request_addr, - } - - # IMPLEMENTATION OF LONGPOLLING - # Times out 2 seconds before the JS request does - if HardwareScreen.event_data.wait(28): - HardwareScreen.event_data.clear() - HardwareScreen.failure_count[request_addr] = 0 - return result - return { - 'rendered_html': False, - 'ip_from': HardwareScreen.pos_client_data['ip_from'], - } - - def _get_html(self): - cust_js = None - interfaces = ni.interfaces() - HardwareScreen.display_in_use = http.request.httprequest.remote_addr - - with open(os.path.join(os.path.dirname(__file__), "../static/src/js/worker.js")) as js: - cust_js = js.read() - - display_ifaces = [] - for iface_id in interfaces: - if 'wlan' in iface_id or 'eth' in iface_id: - iface_obj = ni.ifaddresses(iface_id) - ifconfigs = iface_obj.get(ni.AF_INET, []) - for conf in ifconfigs: - if conf.get('addr'): - display_ifaces.append({ - 'iface_id': iface_id, - 'addr': conf.get('addr'), - 'icon': 'sitemap' if 'eth' in iface_id else 'wifi', - }) - - return pos_display_template.render({ - 'title': "Odoo -- Point of Sale", - 'breadcrumb': 'POS Client display', - 'cust_js': cust_js, - 'display_ifaces': display_ifaces, - }) 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 f633be2b7c7..73f9b777c88 100755 --- a/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init.d/odoo +++ b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init.d/odoo @@ -26,7 +26,7 @@ test -x $DAEMON || exit 0 set -e function _start() { - start-stop-daemon --start --quiet --pidfile $PIDFILE --chuid $USER:$USER --background --make-pidfile --exec $DAEMON -- --config $CONFIG --logfile $LOGFILE --load=web,hw_proxy,hw_posbox_homepage,hw_posbox_upgrade,hw_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_escpos,hw_blackbox_be,hw_drivers } function _stop() {