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 = $('