diff --git a/addons/hw_drivers/__init__.py b/addons/hw_drivers/__init__.py new file mode 100644 index 00000000000..a03bfd097a6 --- /dev/null +++ b/addons/hw_drivers/__init__.py @@ -0,0 +1 @@ +from . import controllers \ No newline at end of file diff --git a/addons/hw_drivers/__manifest__.py b/addons/hw_drivers/__manifest__.py new file mode 100644 index 00000000000..c1955ee6d74 --- /dev/null +++ b/addons/hw_drivers/__manifest__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'Hardware Proxy', + 'category': 'IOT', + 'sequence': 6, + 'summary': 'Connect the Web Client to Hardware Peripherals', + 'website': 'https://www.odoo.com/page/iot', + '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_drivers/controllers/__init__.py b/addons/hw_drivers/controllers/__init__.py new file mode 100644 index 00000000000..3bd726d7111 --- /dev/null +++ b/addons/hw_drivers/controllers/__init__.py @@ -0,0 +1,2 @@ +from . import driver +from . import loading \ No newline at end of file diff --git a/addons/hw_drivers/controllers/driver.py b/addons/hw_drivers/controllers/driver.py new file mode 100755 index 00000000000..7e2a775e4b7 --- /dev/null +++ b/addons/hw_drivers/controllers/driver.py @@ -0,0 +1,422 @@ +#!/usr/bin/python3 +import logging +import time +from threading import Thread +import usb +import gatt +import subprocess +import netifaces as ni +import json +import re +from odoo import http +import urllib3 +from odoo.http import request as httprequest +import datetime + +logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s %(name)s: %(message)s') +_logger = logging.getLogger('dispatcher') + +owner_dict = {} +last_ping = {} + +class StatusController(http.Controller): + + @http.route('/hw_drivers/owner/check', type='json', auth='none', cors='*', csrf=False) + def check_cantakeowner(self): #, devices, tab + data = httprequest.jsonrequest + for device in data['devices']: + if owner_dict.get(device) and owner_dict[device] != data['tab']: + before_date = datetime.datetime.now() - datetime.timedelta(seconds=10) + if last_ping.get(owner_dict[device]) and last_ping.get(owner_dict[device]) > before_date: + return 'no' + else: + old_tab = owner_dict[device] + for dev2 in owner_dict: + if owner_dict[dev2] == old_tab: + owner_dict[dev2] = '' + return 'yes' + + @http.route('/hw_drivers/owner/take', type='json', auth='none', cors='*', csrf=False) + def take_ownership(self): #, devices, tab + data = httprequest.jsonrequest + for device in data['devices']: + owner_dict[device] = data['tab'] + last_ping[data['tab']] = datetime.datetime.now() + return data['tab'] + + @http.route('/hw_drivers/owner/ping', type='json', auth='none', cors='*', csrf=False) + def ping_trigger(self): #, tab + data = httprequest.jsonrequest + ping_dict = {} + last_ping[data['tab']] = datetime.datetime.now() + for dev in data['devices']: + if owner_dict.get(dev) and owner_dict[dev] == data['tab']: + for driver_path in drivers: + if driver_path.find(dev) == 0 and drivers[driver_path].ping_value: + ping_dict[dev] = drivers[driver_path].ping_value + drivers[driver_path].ping_value = '' # or set it to nothing + else: + ping_dict[dev] = 'STOP' + return ping_dict + + @http.route('/hw_drivers/box/connect', type='json', auth='none', cors='*', csrf=False) + def connect_box(self): + data = httprequest.jsonrequest + server = "" # read from file + try: + f = open('/home/pi/odoo-remote-server.conf', 'r') + for line in f: + server += line + f.close() + server = server.split('\n')[0] + except: + server = '' + if server: + return 'This IoTBox has already been connected' + else: + iotname = '' + token = data['token'].split('|')[1] + url = data['token'].split('|')[0] + reboot = 'noreboot' + subprocess.call(['/home/pi/odoo/addons/point_of_sale/tools/posbox/configuration/connect_to_server.sh', url, iotname, token, reboot]) + send_iot_box_device(False) + return 'IoTBox connected' + + @http.route('/hw_drivers/drivers/status', type='http', auth='none', cors='*') + def status(self): + result = "List of drivers and values:
" + result +=" " + return result + + @http.route('/hw_drivers/driverdetails/', type='http', auth='none', cors='*') + def statusdetail(self, identifier): + for device in drivers: + if device.find(identifier) != -1: + return str(drivers[device].value) + return '' + + @http.route('/hw_drivers/driveraction/', type='json', auth='none', cors='*', csrf=False) + def driveraction(self, identifier): + data = httprequest.jsonrequest + result = 'device not found' + if data.get('action') == 'print': + with open('/tmp/toprinter', 'w') as file: + file.write(data['data']) + subprocess.call("cat /tmp/toprinter | base64 -d | lp -d " + identifier, shell=True) + result = 'ok' + if data.get('action') == 'camera': + cameras = subprocess.check_output("v4l2-ctl --list-devices", shell=True).decode('utf-8').split('\n\n') + adrress = '/dev/video0' + for camera in cameras: + if camera: + camera = camera.split('\n\t') + serial = re.sub('[^a-zA-Z0-9 ]+', '', camera[0].split(': ')[0]).replace(' ','_') + if serial == data.get('identifier'): + adrress = camera[1] + picture = subprocess.check_output("v4l2-ctl --list-formats-ext|grep 'Size'|awk '{print $3}'|sort -rn|awk NR==1", shell=True).decode('utf-8') + subprocess.call("fswebcam -d " + adrress + " /tmp/testimage -r " + picture, shell=True) + image_bytes = subprocess.check_output('cat /tmp/testimage | base64',shell=True) + result = {'image': image_bytes} + return result + + @http.route('/hw_drivers/send_iot_box', type='http', auth='none', cors='*') + def send_iot_box(self): + send_iot_box_device(False) + return 'ok' + +#---------------------------------------------------------- +# Driver common interface +#---------------------------------------------------------- +class Driver(Thread): + ping_value = "" + + def supported(self): + pass + + def value(self): + pass + + def get_name(self): + pass + + def get_connection(self): + pass + + def action(self, action): + pass + +#---------------------------------------------------------- +# Usb drivers +#---------------------------------------------------------- +usbdrivers = [] +drivers = {} + +class UsbMetaClass(type): + def __new__(cls, clsname, bases, attrs): + newclass = super(UsbMetaClass, cls).__new__(cls, clsname, bases, attrs) + usbdrivers.append(newclass) + return newclass + +class USBDriver(Driver,metaclass=UsbMetaClass): + def __init__(self, dev): + super(USBDriver, self).__init__() + self.dev = dev + self.value = "" + + def get_name(self): + lsusb = str(subprocess.check_output('lsusb')).split("\\n") + for usbpath in lsusb: # Should filter on usb devices or inverse loops? + device = self.dev + if "%04x:%04x" % (device.idVendor, device.idProduct) in usbpath: + return usbpath.split("%04x:%04x" % (device.idVendor, device.idProduct))[1] + return str(device.idVendor) + ":" + str(device.idProduct) + + def get_connection(self): + return 'direct' + + def value(self): + return self.value + + +#---------------------------------------------------------- +# Bluetooth +#---------------------------------------------------------- +class GattBtManager(gatt.DeviceManager): + + def device_discovered(self, device): + # TODO: need some kind of updated_devices mechanism or not? + path = "bt_%s" % (device.mac_address,) + if path not in drivers: + for driverclass in btdrivers: + d = driverclass(device = device, manager=self) + if d.supported(): + drivers[path] = d + d.connect() + send_iot_box_device(False) + + +class BtManager(Thread): + gatt_manager = False + + def run(self): + dm = GattBtManager(adapter_name='hci0') + self.gatt_manager = dm + dm.start_discovery() + dm.run() + +#---------------------------------------------------------- +# Bluetooth drivers +#---------------------------------------------------------- +btdrivers = [] + +class BtMetaClass(type): + def __new__(cls, clsname, bases, attrs): + newclass = super(BtMetaClass, cls).__new__(cls, clsname, bases, attrs) + btdrivers.append(newclass) + return newclass + + +class BtDriver(Driver, metaclass=BtMetaClass): + + + def __init__(self, device, manager): + super(BtDriver, self).__init__() + self.dev = device + self.manager = manager + self.value = '' + self.gatt_device = False + + def disconnect(self): + path = "bt_%s" % (self.dev.mac_address,) + del drivers[path] + + def get_name(self): + return self.dev.alias() + + def value(self): + return self.value + + def action(self, action): + pass + + def get_connection(self): + return 'bluetooth' + + def connect(self): + pass + + + + + + +class USBDeviceManager(Thread): + devices = {} + def run(self): + first_time = True + send_iot_box_device(False) + while 1: + sendJSON = False + devs = usb.core.find(find_all=True) + updated_devices = {} + for dev in devs: + path = "usb_%04x:%04x_%03d_%03d_" % (dev.idVendor, dev.idProduct, dev.bus, dev.address) + updated_devices[path] = self.devices.get(path, dev) + added = updated_devices.keys() - self.devices.keys() + removed = self.devices.keys() - updated_devices.keys() + self.devices = updated_devices + if (removed): + for path in list(drivers): + if (path in removed): + del drivers[path] + sendJSON = True + for path in added: + dev = updated_devices[path] + for driverclass in usbdrivers: + d = driverclass(updated_devices[path]) + if d.supported(): + _logger.info('For device %s will be driven', path) + drivers[path] = d + # launch thread + d.daemon = True + d.start() + sendJSON = True + if sendJSON or first_time: + send_iot_box_device(send_printer = first_time) + first_time = False + time.sleep(3) + + + + + +def send_iot_box_device(send_printer): + maciotbox = subprocess.check_output("/sbin/ifconfig eth0 |grep -Eo ..\(\:..\){5}", shell=True).decode('utf-8').split('\n')[0] + server = "" # read from file + try: + f = open('/home/pi/odoo-remote-server.conf', 'r') + for line in f: + server += line + f.close() + except: #In case the file does not exist + server='' + server = server.split('\n')[0] + if server: + url = server + "/iot/setup" + interfaces = ni.interfaces() + for iface_id in interfaces: + iface_obj = ni.ifaddresses(iface_id) + ifconfigs = iface_obj.get(ni.AF_INET, []) + for conf in ifconfigs: + if conf.get('addr') and conf.get('addr') != '127.0.0.1': + ips = conf.get('addr') + break + + # Build device JSON + devicesList = {} + for path in drivers: + device_name = drivers[path].get_name() + device_connection = drivers[path].get_connection() + identifier = path.split('_')[0] + '_' + path.split('_')[1] + devicesList[identifier] = {'name': device_name, + 'type': 'device', + 'connection': device_connection} + + # Build camera JSON + try: + cameras = subprocess.check_output("v4l2-ctl --list-devices", shell=True).decode('utf-8').split('\n\n') + for camera in cameras: + if camera: + camera = camera.split('\n\t') + serial = re.sub('[^a-zA-Z0-9 ]+', '', camera[0].split(': ')[0]).replace(' ','_') + devicesList[serial] = { + 'name': camera[0].split(': ')[0], + 'connection': 'direct', + 'type': 'camera' + } + except: + pass + + # Build printer JSON + printerList = {} + if send_printer: + printers = subprocess.check_output("sudo lpinfo -lv", shell=True).decode('utf-8').split('Device') + for printer in printers: + printerTab = printer.split('\n') + if printer and printerTab[4].split('=')[1] != ' ': + device_connection = printerTab[1].split('= ')[1] + model = '' + for device_id in printerTab[4].split('= ')[1].split(';'): + if any(x in device_id for x in ['MDL','MODEL']): + model = device_id.split(':')[1] + name = printerTab[2].split('= ')[1] + serial = re.sub('[^a-zA-Z0-9 ]+', '', model).replace(' ','_') + identifier = '' + if device_connection == 'direct': + identifier = serial + '_' + maciotbox #name + macIOTBOX + elif device_connection == 'network' and 'socket' in printerTab[0]: + socketIP = printerTab[0].split('://')[1] + macprinter = subprocess.check_output("arp -a " + socketIP + " |awk NR==1'{print $4}'", shell=True).decode('utf-8').split('\n')[0] + identifier = macprinter # macPRINTER + elif device_connection == 'network' and 'dnssd' in printerTab[0]: + hostname_printer = subprocess.check_output("ippfind -n \"" + model + "\" | awk \'{split($0,a,\"/\"); print a[3]}\' | awk \'{split($0,b,\":\"); print b[1]}\'", shell=True).decode('utf-8').split('\n')[0] + if hostname_printer: + macprinter = subprocess.check_output("arp -a " + hostname_printer + " |awk NR==1'{print $4}'", shell=True).decode('utf-8').split('\n')[0] + identifier = macprinter # macprinter + + identifier = identifier.replace(':','_') + if identifier and identifier not in printerList: + printerList[identifier] = { + 'name': model, + 'connection': device_connection, + 'type': 'printer' + } + + # install these printers + try: + ppd = subprocess.check_output("sudo lpinfo -m |grep '" + model + "'", shell=True).decode('utf-8').split('\n') + if len(ppd) > 2: + subprocess.call("sudo lpadmin -p '" + identifier + "' -E -v '" + printerTab[0].split('= ')[1] + "'", shell=True) + else: + subprocess.call("sudo lpadmin -p '" + identifier + "' -E -v '" + printerTab[0].split('= ')[1] + "' -m '" + ppd[0].split(' ')[0] + "'", shell=True) + except: + subprocess.call("sudo lpadmin -p '" + identifier + "' -E -v '" + printerTab[0].split('= ')[1] + "'", shell=True) + subprocess.call('> /tmp/printers', shell=True) + for printer in printerList: + subprocess.call('echo "' + printerList[printer]['name'] + '" >> /tmp/printers', shell=True) + + if devicesList: + subprocess.call('> /tmp/devices', shell=True) + for device in devicesList: + subprocess.call('echo "' + str(device) + '|' + devicesList[device]['name'] + '" >> /tmp/devices', shell=True) + + #build JSON with all devices + hostname = subprocess.check_output('hostname').decode('utf-8').split('\n')[0] + token = "" # read from file + try: + f = open('/home/pi/token', 'r') + for line in f: + token += line + f.close() + except: #In case the file does not exist + token='' + token = token.split('\n')[0] + data = {'name': hostname,'identifier': maciotbox, 'ip': ips, 'token': token} + devicesList.update(printerList) + data['devices'] = devicesList + data_json = json.dumps(data).encode('utf8') + headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} + http = urllib3.PoolManager() + req = False + try: + req = http.request('POST', + url, + body=data_json, + headers=headers) + except: + _logger.warning('Could not reach configured server') + + diff --git a/addons/hw_drivers/controllers/loading.py b/addons/hw_drivers/controllers/loading.py new file mode 100644 index 00000000000..cd02d3dd82f --- /dev/null +++ b/addons/hw_drivers/controllers/loading.py @@ -0,0 +1,34 @@ +import importlib.util +import os +from .driver import BtManager +from .driver import USBDeviceManager +from odoo import modules + +driversList = os.listdir("/home/pi/odoo/addons/hw_drivers/drivers") +for driver in driversList: + path = "/home/pi/odoo/addons/hw_drivers/drivers/" + driver + spec = importlib.util.spec_from_file_location(driver, path) + if spec: + foo = importlib.util.module_from_spec(spec) + spec.loader.exec_module(foo) + + +if not getattr(modules, '_iot_daemon_started', False): + # ---------------------------------------------------------- + # Bluetooth start + # ---------------------------------------------------------- + bm = BtManager() + bm.daemon = True + bm.start() + + #---------------------------------------------------------- + #USB start + #---------------------------------------------------------- + + udm = USBDeviceManager() + udm.daemon = True + udm.start() + + # Did this because of the + modules._iot_daemon_started = True +