[ADD] hw_drivers: drivers management module for the IoT Box
- Routes to get device/driver statuses from the browser - Framework for managing and downloading drivers for devices connected to the IoT Box - Automatic detection of printers and cameras in the network - Built-in detection for USB and Bluetooth LE devices and checking if a driver matches the device - Authentication of the box into Odoo - Inform Odoo of the connected devices and giving them a unique identifier so they can be called from the browser Thanks to qle, specially for fswebcam and cups
This commit is contained in:
@@ -0,0 +1 @@
|
||||
from . import controllers
|
||||
@@ -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,
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
from . import driver
|
||||
from . import loading
|
||||
Executable
+422
@@ -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 = "<html><head></head><body>List of drivers and values: <br/> <ul>"
|
||||
for path in drivers:
|
||||
result += "<li>" + path + ":" + str(drivers[path].value) + "</li>"
|
||||
result += "</ul>"
|
||||
result +=" </body></html>"
|
||||
return result
|
||||
|
||||
@http.route('/hw_drivers/driverdetails/<string:identifier>', 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/<string:identifier>', 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')
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user