[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
This commit is contained in:
Antoine Prieels
2019-09-04 13:08:10 +00:00
parent 75020a74e5
commit f03026a3d7
9 changed files with 204 additions and 187 deletions
+25 -2
View File
@@ -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())
+173
View File
@@ -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,
})
@@ -4,7 +4,6 @@
// It allows to transform the <head> 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 = $('<div>').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,
-4
View File
@@ -1,4 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import controllers
-21
View File
@@ -1,21 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': '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,
}
-4
View File
@@ -1,4 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import main
-144
View File
@@ -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,
})
@@ -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() {