Add a button to download the IoT Box log file from the Box form view in the connected Odoo DB. closes odoo/odoo#41093 Taskid: 1943418 Related: odoo/enterprise#6961 Signed-off-by: pimodoo <pimodoo@users.noreply.github.com>
555 lines
20 KiB
Python
Executable File
555 lines
20 KiB
Python
Executable File
#!/usr/bin/python3
|
|
import logging
|
|
import time
|
|
from threading import Thread, Event, Lock
|
|
from usb import core
|
|
from gatt import DeviceManager as Gatt_DeviceManager
|
|
import subprocess
|
|
import json
|
|
from re import sub, finditer
|
|
import urllib3
|
|
import os
|
|
import socket
|
|
import sys
|
|
from importlib import util
|
|
import v4l2
|
|
from fcntl import ioctl
|
|
from cups import Connection as cups_connection
|
|
from glob import glob
|
|
from base64 import b64decode
|
|
from pathlib import Path
|
|
import socket
|
|
import ctypes
|
|
|
|
from odoo import http, tools, _
|
|
from odoo.http import send_file
|
|
from odoo.modules.module import get_resource_path
|
|
from odoo.addons.hw_drivers.tools import helpers
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
#----------------------------------------------------------
|
|
# Controllers
|
|
#----------------------------------------------------------
|
|
|
|
class StatusController(http.Controller):
|
|
@http.route('/hw_drivers/action', type='json', auth='none', cors='*', csrf=False, save_session=False)
|
|
def action(self, session_id, device_id, data):
|
|
"""
|
|
This route is called when we want to make a action with device (take picture, printing,...)
|
|
We specify in data from which session_id that action is called
|
|
And call the action of specific device
|
|
"""
|
|
iot_device = iot_devices.get(device_id)
|
|
if iot_device:
|
|
iot_device.data['owner'] = session_id
|
|
data = json.loads(data)
|
|
iot_device.action(data)
|
|
return True
|
|
return False
|
|
|
|
@http.route('/hw_drivers/check_certificate', type='http', auth='none', cors='*', csrf=False, save_session=False)
|
|
def check_certificate(self):
|
|
"""
|
|
This route is called when we want to check if certificate is up-to-date
|
|
Used in cron.daily
|
|
"""
|
|
helpers.check_certificate()
|
|
|
|
@http.route('/hw_drivers/event', type='json', auth='none', cors='*', csrf=False, save_session=False)
|
|
def event(self, listener):
|
|
"""
|
|
listener is a dict in witch there are a sessions_id and a dict of device_id to listen
|
|
"""
|
|
req = event_manager.add_request(listener)
|
|
if req['event'].wait(50):
|
|
req['event'].clear()
|
|
req['result']['session_id'] = req['session_id']
|
|
return req['result']
|
|
|
|
@http.route('/hw_drivers/box/connect', type='http', auth='none', cors='*', csrf=False, save_session=False)
|
|
def connect_box(self, token):
|
|
"""
|
|
This route is called when we want that a IoT Box will be connected to a Odoo DB
|
|
token is a base 64 encoded string and have 2 argument separate by |
|
|
1 - url of odoo DB
|
|
2 - token. This token will be compared to the token of Odoo. He have 1 hour lifetime
|
|
"""
|
|
server = helpers.get_odoo_server_url()
|
|
image = get_resource_path('hw_drivers', 'static/img', 'False.jpg')
|
|
if not server:
|
|
credential = b64decode(token).decode('utf-8').split('|')
|
|
url = credential[0]
|
|
token = credential[1]
|
|
if len(credential) > 2:
|
|
# IoT Box send token with db_uuid and enterprise_code only since V13
|
|
db_uuid = credential[2]
|
|
enterprise_code = credential[3]
|
|
helpers.add_credential(db_uuid, enterprise_code)
|
|
try:
|
|
subprocess.check_call([get_resource_path('point_of_sale', 'tools/posbox/configuration/connect_to_server.sh'), url, '', token, 'noreboot'])
|
|
helpers.check_certificate()
|
|
m.send_alldevices()
|
|
m.load_drivers()
|
|
image = get_resource_path('hw_drivers', 'static/img', 'True.jpg')
|
|
except subprocess.CalledProcessError as e:
|
|
_logger.error('A error encountered : %s ' % e.output)
|
|
if os.path.isfile(image):
|
|
with open(image, 'rb') as f:
|
|
return f.read()
|
|
|
|
@http.route('/hw_drivers/download_logs', type='http', auth='none', cors='*', csrf=False, save_session=False)
|
|
def download_logs(self):
|
|
"""
|
|
Downloads the log file
|
|
"""
|
|
if tools.config['logfile']:
|
|
res = send_file(tools.config['logfile'], mimetype="text/plain", as_attachment=True)
|
|
res.headers['Cache-Control'] = 'no-cache'
|
|
return res
|
|
|
|
#----------------------------------------------------------
|
|
# Log Exceptions
|
|
#----------------------------------------------------------
|
|
|
|
class ExceptionLogger:
|
|
"""
|
|
Redirect Exceptions to the logger to keep track of them in the log file.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.logger = logging.getLogger()
|
|
|
|
def write(self, message):
|
|
if message != '\n':
|
|
self.logger.err(message)
|
|
|
|
def flush(self):
|
|
pass
|
|
|
|
sys.stderr = ExceptionLogger()
|
|
|
|
#----------------------------------------------------------
|
|
# Drivers
|
|
#----------------------------------------------------------
|
|
|
|
drivers = []
|
|
bt_devices = {}
|
|
socket_devices = {}
|
|
iot_devices = {}
|
|
|
|
class DriverMetaClass(type):
|
|
def __new__(cls, clsname, bases, attrs):
|
|
newclass = super(DriverMetaClass, cls).__new__(cls, clsname, bases, attrs)
|
|
# Some drivers must be tried only when all the others have been ruled out. These are kept at the bottom of the list.
|
|
if newclass.is_tested_last:
|
|
drivers.append(newclass)
|
|
else:
|
|
drivers.insert(0, newclass)
|
|
return newclass
|
|
|
|
class Driver(Thread, metaclass=DriverMetaClass):
|
|
"""
|
|
Hook to register the driver into the drivers list
|
|
"""
|
|
connection_type = ""
|
|
is_tested_last = False
|
|
|
|
def __init__(self, device):
|
|
super(Driver, self).__init__()
|
|
self.dev = device
|
|
self.data = {'value': ''}
|
|
self.gatt_device = False
|
|
self._device_manufacturer = ''
|
|
|
|
@property
|
|
def device_name(self):
|
|
return self._device_name
|
|
|
|
@property
|
|
def device_identifier(self):
|
|
return self.dev.identifier
|
|
|
|
@property
|
|
def device_manufacturer(self):
|
|
return self._device_manufacturer
|
|
|
|
@property
|
|
def device_connection(self):
|
|
"""
|
|
On specific driver override this method to give connection type of device
|
|
return string
|
|
possible value : direct - network - bluetooth - serial - hdmi
|
|
"""
|
|
return self._device_connection
|
|
|
|
@property
|
|
def device_type(self):
|
|
"""
|
|
On specific driver override this method to give type of device
|
|
return string
|
|
possible value : printer - camera - keyboard - scanner - display - device
|
|
"""
|
|
return self._device_type
|
|
|
|
@classmethod
|
|
def supported(cls, device):
|
|
"""
|
|
On specific driver override this method to check if device is supported or not
|
|
return True or False
|
|
"""
|
|
pass
|
|
|
|
def get_message(self):
|
|
return ''
|
|
|
|
def action(self, data):
|
|
"""
|
|
On specific driver override this method to make a action with device (take picture, printing,...)
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def disconnect(self):
|
|
del iot_devices[self.device_identifier]
|
|
|
|
|
|
#----------------------------------------------------------
|
|
# Device manager
|
|
#----------------------------------------------------------
|
|
|
|
class EventManager(object):
|
|
def __init__(self):
|
|
self.sessions = {}
|
|
|
|
def _delete_expired_sessions(self, max_time=70):
|
|
'''
|
|
Clears sessions that are no longer called.
|
|
|
|
:param max_time: time a session can stay unused before being deleted
|
|
'''
|
|
now = time.time()
|
|
expired_sessions = [session for session in self.sessions if now - self.sessions[session]['time_request'] > max_time]
|
|
for session in expired_sessions:
|
|
del self.sessions[session]
|
|
|
|
def add_request(self, listener):
|
|
self.session = {
|
|
'session_id': listener['session_id'],
|
|
'devices': listener['devices'],
|
|
'event': Event(),
|
|
'result': {},
|
|
'time_request': time.time(),
|
|
}
|
|
self._delete_expired_sessions()
|
|
self.sessions[listener['session_id']] = self.session
|
|
return self.sessions[listener['session_id']]
|
|
|
|
def device_changed(self, device):
|
|
for session in self.sessions:
|
|
if device.device_identifier in self.sessions[session]['devices']:
|
|
self.sessions[session]['result'] = device.data
|
|
self.sessions[session]['result']['device_id'] = device.device_identifier
|
|
self.sessions[session]['event'].set()
|
|
|
|
|
|
class IoTDevice(object):
|
|
|
|
def __init__(self, dev, connection_type):
|
|
self.dev = dev
|
|
self.connection_type = connection_type
|
|
|
|
event_manager = EventManager()
|
|
|
|
|
|
#----------------------------------------------------------
|
|
# Manager
|
|
#----------------------------------------------------------
|
|
|
|
class Manager(Thread):
|
|
|
|
def load_drivers(self):
|
|
"""
|
|
This method loads local files: 'odoo/addons/hw_drivers/drivers'
|
|
And execute these python drivers
|
|
"""
|
|
helpers.download_drivers()
|
|
path = get_resource_path('hw_drivers', 'drivers')
|
|
driversList = os.listdir(path)
|
|
self.devices = {}
|
|
for driver in driversList:
|
|
path_file = os.path.join(path, driver)
|
|
spec = util.spec_from_file_location(driver, path_file)
|
|
if spec:
|
|
module = util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
http.addons_manifest = {}
|
|
http.root = http.Root()
|
|
|
|
def send_alldevices(self):
|
|
"""
|
|
This method send IoT Box and devices informations to Odoo database
|
|
"""
|
|
server = helpers.get_odoo_server_url()
|
|
if server:
|
|
subject = helpers.read_file_first_line('odoo-subject.conf')
|
|
if subject:
|
|
domain = helpers.get_ip().replace('.', '-') + subject.strip('*')
|
|
else:
|
|
domain = helpers.get_ip()
|
|
iot_box = {
|
|
'name': socket.gethostname(),
|
|
'identifier': helpers.get_mac_address(),
|
|
'ip': domain,
|
|
'token': helpers.get_token(),
|
|
'version': helpers.get_version()
|
|
}
|
|
devices_list = {}
|
|
for device in iot_devices:
|
|
identifier = iot_devices[device].device_identifier
|
|
devices_list[identifier] = {
|
|
'name': iot_devices[device].device_name,
|
|
'type': iot_devices[device].device_type,
|
|
'manufacturer': iot_devices[device].device_manufacturer,
|
|
'connection': iot_devices[device].device_connection,
|
|
}
|
|
data = {
|
|
'params': {
|
|
'iot_box' : iot_box,
|
|
'devices' : devices_list,
|
|
}
|
|
}
|
|
# disable certifiacte verification
|
|
urllib3.disable_warnings()
|
|
http = urllib3.PoolManager(cert_reqs='CERT_NONE')
|
|
try:
|
|
http.request(
|
|
'POST',
|
|
server + "/iot/setup",
|
|
body = json.dumps(data).encode('utf8'),
|
|
headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
|
|
)
|
|
except Exception as e:
|
|
_logger.error('Could not reach configured server')
|
|
_logger.error('A error encountered : %s ' % e)
|
|
else:
|
|
_logger.warning('Odoo server not set')
|
|
|
|
def get_connected_displays(self):
|
|
display_devices = {}
|
|
|
|
displays = subprocess.check_output(['tvservice', '-l']).decode()
|
|
x_screen = 0
|
|
for match in finditer('Display Number (\d), type HDMI (\d)', displays):
|
|
display_id, hdmi_id = match.groups()
|
|
display_name = subprocess.check_output(['tvservice', '-nv', display_id]).decode().rstrip().split('=')[1]
|
|
display_identifier = sub('[^a-zA-Z0-9 ]+', '', display_name).replace(' ', '_') + "_" + str(hdmi_id)
|
|
iot_device = IoTDevice({
|
|
'identifier': display_identifier,
|
|
'name': display_name,
|
|
'x_screen': str(x_screen),
|
|
}, 'display')
|
|
display_devices[display_identifier] = iot_device
|
|
x_screen += 1
|
|
|
|
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/*'):
|
|
iot_device = IoTDevice({'identifier': identifier, }, 'serial')
|
|
serial_devices[identifier] = iot_device
|
|
return serial_devices
|
|
|
|
def usb_loop(self):
|
|
"""
|
|
Loops over the connected usb devices, assign them an identifier, instantiate
|
|
an `IoTDevice` for them.
|
|
|
|
USB devices are identified by a combination of their `idVendor` and
|
|
`idProduct`. We can't be sure this combination in unique per equipment.
|
|
To still allow connecting multiple similar equipments, we complete the
|
|
identifier by a counter. The drawbacks are we can't be sure the equipments
|
|
will get the same identifiers after a reboot or a disconnect/reconnect.
|
|
|
|
:return: a dict of the `IoTDevices` instances indexed by their identifier.
|
|
"""
|
|
usb_devices = {}
|
|
devs = core.find(find_all=True)
|
|
cpt = 2
|
|
for dev in devs:
|
|
dev.identifier = "usb_%04x:%04x" % (dev.idVendor, dev.idProduct)
|
|
if dev.identifier in usb_devices:
|
|
dev.identifier += '_%s' % cpt
|
|
cpt += 1
|
|
iot_device = IoTDevice(dev, 'usb')
|
|
usb_devices[dev.identifier] = iot_device
|
|
return usb_devices
|
|
|
|
def video_loop(self):
|
|
camera_devices = {}
|
|
videos = glob('/dev/video*')
|
|
for video in videos:
|
|
with open(video, 'w') as path:
|
|
dev = v4l2.v4l2_capability()
|
|
ioctl(path, v4l2.VIDIOC_QUERYCAP, dev)
|
|
dev.interface = video
|
|
dev.identifier = dev.bus_info.decode('utf-8')
|
|
iot_device = IoTDevice(dev, 'video')
|
|
camera_devices[dev.identifier] = iot_device
|
|
return camera_devices
|
|
|
|
def printer_loop(self):
|
|
printer_devices = {}
|
|
with cups_lock:
|
|
devices = conn.getDevices()
|
|
for path in devices:
|
|
if 'uuid=' in path:
|
|
serial = sub('[^a-zA-Z0-9 ]+', '', path.split('uuid=')[1])
|
|
elif 'serial=' in path:
|
|
serial = sub('[^a-zA-Z0-9 ]+', '', path.split('serial=')[1])
|
|
else:
|
|
serial = sub('[^a-zA-Z0-9 ]+', '', path)
|
|
devices[path]['identifier'] = serial
|
|
devices[path]['url'] = path
|
|
iot_device = IoTDevice(devices[path], 'printer')
|
|
printer_devices[serial] = iot_device
|
|
return printer_devices
|
|
|
|
def run(self):
|
|
"""
|
|
Thread that will check connected/disconnected device, load drivers if needed and contact the odoo server with the updates
|
|
"""
|
|
helpers.check_git_branch()
|
|
helpers.check_certificate()
|
|
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())
|
|
if cpt % 40 == 0:
|
|
printer_devices = self.printer_loop()
|
|
cpt = 0
|
|
updated_devices.update(printer_devices)
|
|
cpt += 1
|
|
added = updated_devices.keys() - self.devices.keys()
|
|
removed = self.devices.keys() - updated_devices.keys()
|
|
self.devices = updated_devices
|
|
send_devices = False
|
|
for path in [device_rm for device_rm in removed if device_rm in iot_devices]:
|
|
iot_devices[path].disconnect()
|
|
_logger.info('Device %s is now disconnected', path)
|
|
send_devices = True
|
|
for path in [device_add for device_add in added if device_add not in iot_devices]:
|
|
for driverclass in [d for d in drivers if d.connection_type == self.devices[path].connection_type]:
|
|
if driverclass.supported(device = updated_devices[path].dev):
|
|
_logger.info('Device %s is now connected', path)
|
|
d = driverclass(device = updated_devices[path].dev)
|
|
d.daemon = True
|
|
d.start()
|
|
iot_devices[path] = d
|
|
send_devices = True
|
|
break
|
|
if send_devices:
|
|
self.send_alldevices()
|
|
time.sleep(3)
|
|
|
|
class GattBtManager(Gatt_DeviceManager):
|
|
|
|
def device_discovered(self, device):
|
|
path = "bt_%s" % (device.mac_address,)
|
|
if path not in bt_devices:
|
|
device.manager = self
|
|
device.identifier = path
|
|
iot_device = IoTDevice(device, 'bluetooth')
|
|
bt_devices[path] = iot_device
|
|
|
|
class BtManager(Thread):
|
|
|
|
def run(self):
|
|
dm = GattBtManager(adapter_name='hci0')
|
|
for device in [device_con for device_con in dm.devices() if device_con.is_connected()]:
|
|
device.disconnect()
|
|
dm.start_discovery()
|
|
dm.run()
|
|
|
|
class SocketManager(Thread):
|
|
|
|
def run(self):
|
|
while True:
|
|
try:
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
sock.bind(('', 9000))
|
|
sock.listen(1)
|
|
dev, addr = sock.accept()
|
|
if addr and addr[0] not in socket_devices:
|
|
iot_device = IoTDevice(type('', (), {'dev': dev}), 'socket')
|
|
socket_devices[addr[0]] = iot_device
|
|
except OSError as e:
|
|
_logger.error(_('Error in SocketManager: %s') % (e.strerror))
|
|
|
|
class MPDManager(Thread):
|
|
def __init__(self):
|
|
super(MPDManager, self).__init__()
|
|
self.devices = {}
|
|
self.mpd_session = ctypes.c_void_p()
|
|
|
|
def run(self):
|
|
eftapi.EFT_CreateSession(ctypes.byref(self.mpd_session))
|
|
while True:
|
|
if self.terminal_connected(terminal_id):
|
|
self.devices[terminal_id] = IoTDevice(terminal_id, 'mpd')
|
|
elif terminal_id in self.devices:
|
|
self.devices = {}
|
|
time.sleep(20)
|
|
|
|
def terminal_connected(self, terminal_id):
|
|
eftapi.EFT_QueryStatus(self.mpd_session)
|
|
device_status = ctypes.c_long()
|
|
eftapi.EFT_GetDeviceStatusCode(self.mpd_session, ctypes.byref(device_status))
|
|
return device_status.value in [0, 1]
|
|
|
|
|
|
conn = cups_connection()
|
|
PPDs = conn.getPPDs()
|
|
printers = conn.getPrinters()
|
|
cups_lock = Lock() # We can only make one call to Cups at a time
|
|
|
|
mpdm = MPDManager()
|
|
terminal_id = helpers.read_file_first_line('odoo-six-payment-terminal.conf')
|
|
subprocess.call(["pkill", "-9", "eftdvs"]) # Kill any running MPD server
|
|
if terminal_id:
|
|
subprocess.Popen(["eftdvs", "/ConfigDir", "/usr/share/eftdvs/", "/TID", terminal_id]) # Start MPD server
|
|
eftapi = ctypes.CDLL("eftapi.so") # Library given by Six
|
|
mpdm.daemon = True
|
|
mpdm.start()
|
|
|
|
m = Manager()
|
|
m.daemon = True
|
|
m.start()
|
|
|
|
bm = BtManager()
|
|
bm.daemon = True
|
|
bm.start()
|
|
|
|
sm = SocketManager()
|
|
sm.daemon = True
|
|
sm.start()
|