From 104283568551a23872b69cedd9e4e8d16865bcc9 Mon Sep 17 00:00:00 2001 From: Piotr Dobrowolski Date: Mon, 10 Sep 2018 13:40:33 +0200 Subject: [PATCH] Initial refactor, zeroconf tests --- Makefile | 2 +- luftdaten-tool.py | 115 +++++-------------------------------- luftdatentool/__init__.py | 0 luftdatentool/consts.py | 18 ++++++ luftdatentool/qtvariant.py | 8 +++ luftdatentool/utils.py | 56 ++++++++++++++++++ luftdatentool/workers.py | 53 +++++++++++++++++ requirements.txt | 2 + 8 files changed, 152 insertions(+), 102 deletions(-) create mode 100644 luftdatentool/__init__.py create mode 100644 luftdatentool/consts.py create mode 100644 luftdatentool/qtvariant.py create mode 100644 luftdatentool/utils.py create mode 100644 luftdatentool/workers.py diff --git a/Makefile b/Makefile index b4ad471..a690f8b 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ UI_FILES = $(wildcard gui/*.ui) TS_FILES = $(wildcard i18n/*.ts) -PY_FILES = $(wildcard *.py) $(wildcard gui/*.py) +PY_FILES = $(wildcard *.py) $(wildcard gui/*.py) $(wildcard luftdatentool/*.py) UI_COMPILED = $(UI_FILES:.ui=.py) TS_COMPILED = $(TS_FILES:.ts=.qm) diff --git a/luftdaten-tool.py b/luftdaten-tool.py index 47cc787..debc4c9 100644 --- a/luftdaten-tool.py +++ b/luftdaten-tool.py @@ -2,121 +2,33 @@ import sys import os.path -import re import time import tempfile import hashlib import zlib import logging -import serial -import serial.tools.list_ports import requests from esptool import ESPLoader -from PyQt5 import QtGui, QtCore, QtWidgets + +from luftdatentool.qtvariant import QtGui, QtCore, QtWidgets +from luftdatentool.utils import QuickThread +from luftdatentool.workers import PortDetectThread, FirmwareListThread, \ + ZeroconfDiscoveryThread from gui import mainwindow -# Firmware update repository -UPDATE_REPOSITORY = 'https://www.madavi.de/sensor/update/data/' - -# URI prefixes (protocol parts, essentially) to be downloaded using requests -ALLOWED_PROTO = ('http://', 'https://') - -# vid/pid pairs of known NodeMCU/ESP8266 development boards -PREFERED_PORTS = [ - # CH341 - (0x1A86, 0x7523), - - # CP2102 - (0x10c4, 0xea60), -] - -ROLE_DEVICE = QtCore.Qt.UserRole + 1 +from luftdatentool.consts import UPDATE_REPOSITORY, ALLOWED_PROTO, \ + PREFERED_PORTS, ROLE_DEVICE if getattr(sys, 'frozen', False): RESOURCES_PATH = sys._MEIPASS else: RESOURCES_PATH = os.path.dirname(os.path.realpath(__file__)) -# FIXME move this into something like qtvariant.py -QtCore.Signal = QtCore.pyqtSignal -QtCore.Slot = QtCore.pyqtSlot - -file_index_re = re.compile(r'([^<]*)') - - -def indexof(path): - """Returns list of filenames parsed off "Index of" page""" - resp = requests.get(path) - return [a for a, b in file_index_re.findall(resp.text) if a == b] - - -class QuickThread(QtCore.QThread): - """Provides similar API to threading.Thread but with additional error - reporting based on Qt Signals""" - def __init__(self, parent=None, target=None, args=None, kwargs=None, - error=None): - super(QuickThread, self).__init__(parent) - self.target = target - self.args = args or [] - self.kwargs = kwargs or {} - self.error = error - - def run(self): - try: - self.target(*self.args, **self.kwargs) - except Exception as exc: - if self.error: - self.error.emit(str(exc)) - # raise here causes windows builds to just die. ¯\_(ツ)_/¯ - logging.exception('Unhandled exception') - - @classmethod - def wrap(cls, func): - """Decorator that wraps function in a QThread. Calling resulting - function starts and creates QThread, with parent set to [self]""" - def wrapped(*args, **kwargs): - th = cls(parent=args[0], target=func, args=args, kwargs=kwargs, - error=kwargs.pop('error', None)) - func._th = th - th.start() - - return th - - wrapped.running = lambda: (hasattr(func, '_th') and - func._th.isRunning()) - return wrapped - - -class PortDetectThread(QtCore.QThread): - interval = 1.0 - portsUpdate = QtCore.Signal([list]) - - def run(self): - """Checks list of available ports and emits signal when necessary""" - - ports = [] - while True: - new_ports = serial.tools.list_ports.comports() - - if [p.name for p in ports] != [p.name for p in new_ports]: - self.portsUpdate.emit(new_ports) - - time.sleep(self.interval) - - ports = new_ports - - -class FirmwareListThread(QtCore.QThread): - onFirmware = QtCore.Signal([list]) - - def run(self): - """Downloads list of available firmware updates in separate thread.""" - self.onFirmware.emit(list(indexof(UPDATE_REPOSITORY))) class MainWindow(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow): - signal = QtCore.Signal([str, int]) + uploadProgress = QtCore.Signal([str, int]) errorSignal = QtCore.Signal([str]) uploadThread = None @@ -136,17 +48,20 @@ class MainWindow(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow): self.i18n_init(QtCore.QLocale.system()) self.statusbar.showMessage(self.tr("Loading firmware list...")) + self.firmware_list = FirmwareListThread() - self.firmware_list.onFirmware.connect(self.populate_versions) + self.firmware_list.listLoaded.connect(self.populate_versions) + self.firmware_list.error.connect(self.on_work_error) self.firmware_list.start() self.port_detect = PortDetectThread() self.port_detect.portsUpdate.connect(self.populate_boards) + self.port_detect.error.connect(self.on_work_error) self.port_detect.start() self.on_expertModeBox_clicked() - self.signal.connect(self.on_work_update) + self.uploadProgress.connect(self.on_work_update) self.errorSignal.connect(self.on_work_error) self.cachedir = tempfile.TemporaryDirectory() @@ -257,7 +172,7 @@ class MainWindow(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow): self.statusbar.showMessage(self.tr("Work in progess...")) return - self.flash_board(self.signal, device, binary_uri, + self.flash_board(self.uploadProgress, device, binary_uri, error=self.errorSignal) def cache_download(self, progress, binary_uri): @@ -291,8 +206,6 @@ class MainWindow(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow): if binary_uri.startswith(ALLOWED_PROTO): binary_uri = self.cache_download(progress, binary_uri) - print(binary_uri) - progress.emit(self.tr('Connecting...'), 0) init_baud = min(ESPLoader.ESP_ROM_BAUD, baudrate) diff --git a/luftdatentool/__init__.py b/luftdatentool/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/luftdatentool/consts.py b/luftdatentool/consts.py new file mode 100644 index 0000000..ab53ae5 --- /dev/null +++ b/luftdatentool/consts.py @@ -0,0 +1,18 @@ +from .qtvariant import QtCore + +# Firmware update repository +UPDATE_REPOSITORY = 'https://www.madavi.de/sensor/update/data/' + +# URI prefixes (protocol parts, essentially) to be downloaded using requests +ALLOWED_PROTO = ('http://', 'https://') + +# vid/pid pairs of known NodeMCU/ESP8266 development boards +PREFERED_PORTS = [ + # CH341 + (0x1A86, 0x7523), + + # CP2102 + (0x10c4, 0xea60), +] + +ROLE_DEVICE = QtCore.Qt.UserRole + 1 diff --git a/luftdatentool/qtvariant.py b/luftdatentool/qtvariant.py new file mode 100644 index 0000000..aae48f2 --- /dev/null +++ b/luftdatentool/qtvariant.py @@ -0,0 +1,8 @@ +"""PyQt5 & PySide2 compatiblity layer stub. This will be updated when PySide2 +gets mature enough""" + +from PyQt5 import QtGui, QtCore, QtWidgets + +# Replace nonsense prefixes +QtCore.Signal = QtCore.pyqtSignal +QtCore.Slot = QtCore.pyqtSlot diff --git a/luftdatentool/utils.py b/luftdatentool/utils.py new file mode 100644 index 0000000..e3986a4 --- /dev/null +++ b/luftdatentool/utils.py @@ -0,0 +1,56 @@ +import requests +import re +import logging + +from .qtvariant import QtCore + + +file_index_re = re.compile(r'([^<]*)') + + +def indexof(path): + """Returns list of filenames parsed off "Index of" page""" + resp = requests.get(path) + return [a for a, b in file_index_re.findall(resp.text) if a == b] + + +class QuickThread(QtCore.QThread): + error = QtCore.Signal([str]) + + """Provides similar API to threading.Thread but with additional error + reporting based on Qt Signals""" + def __init__(self, parent=None, target=None, args=None, kwargs=None, + error=None): + super(QuickThread, self).__init__(parent) + self.target = target or self.target + self.args = args or [] + self.kwargs = kwargs or {} + self.error = error or self.error + + def run(self): + try: + self.target(*self.args, **self.kwargs) + except Exception as exc: + if self.error: + self.error.emit(str(exc)) + # raise here causes windows builds to just die. ¯\_(ツ)_/¯ + logging.exception('Unhandled exception') + + @classmethod + def wrap(cls, func): + """Decorator that wraps function in a QThread. Calling resulting + function starts and creates QThread, with parent set to [self]""" + def wrapped(*args, **kwargs): + th = cls(parent=args[0], target=func, args=args, kwargs=kwargs, + error=kwargs.pop('error', None)) + func._th = th + th.start() + + return th + + wrapped.running = lambda: (hasattr(func, '_th') and + func._th.isRunning()) + return wrapped + + def target(self): + pass diff --git a/luftdatentool/workers.py b/luftdatentool/workers.py new file mode 100644 index 0000000..d2aed47 --- /dev/null +++ b/luftdatentool/workers.py @@ -0,0 +1,53 @@ +import time +import socket + +import serial +import serial.tools.list_ports +import zeroconf + +from .qtvariant import QtCore +from .utils import indexof, QuickThread +from .consts import UPDATE_REPOSITORY + + +class PortDetectThread(QuickThread): + interval = 1.0 + portsUpdate = QtCore.Signal([list]) + + def target(self): + """Checks list of available ports and emits signal when necessary""" + + ports = [] + while True: + new_ports = serial.tools.list_ports.comports() + + if [p.name for p in ports] != [p.name for p in new_ports]: + self.portsUpdate.emit(new_ports) + + time.sleep(self.interval) + + ports = new_ports + + +class FirmwareListThread(QuickThread): + listLoaded = QtCore.Signal([list]) + + def target(self): + """Downloads list of available firmware updates in separate thread.""" + self.listLoaded.emit(list(indexof(UPDATE_REPOSITORY))) + + +class ZeroconfDiscoveryThread(QuickThread): + deviceDiscovered = QtCore.Signal(str, str, object) + + def target(self): + zc = zeroconf.Zeroconf() + browser = zeroconf.ServiceBrowser(zc, "_http._tcp.local.", + handlers=[self.on_state_change]) + while True: + time.sleep(0.5) + + def on_state_change(self, zeroconf, service_type, name, state_change): + info = zeroconf.get_service_info(service_type, name) + if info: + self.deviceDiscovered.emit(name, socket.inet_ntoa(info.address), info) diff --git a/requirements.txt b/requirements.txt index d70fe7a..56b4a6b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,7 @@ esptool==2.5.0 future==0.16.0 idna==2.7 macholib==1.11 +netifaces==0.10.7 pefile==2018.8.8 pyaes==1.6.1 https://github.com/pyinstaller/pyinstaller/archive/bbf964c6b89ca33823031fa7ed277c0269192b3e.zip#egg=PyInstaller @@ -14,3 +15,4 @@ PyQt5-sip==4.19.12 pyserial==3.4 requests==2.19.1 urllib3==1.23 +zeroconf==0.20.0