[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:
@@ -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())
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
+5
-11
@@ -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,
|
||||
@@ -1,4 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import controllers
|
||||
@@ -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,
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import main
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user