Initial refactor, zeroconf tests

This commit is contained in:
Piotr Dobrowolski 2018-09-10 13:40:33 +02:00
parent af335bac94
commit 1042835685
8 changed files with 152 additions and 102 deletions

View File

@ -1,6 +1,6 @@
UI_FILES = $(wildcard gui/*.ui) UI_FILES = $(wildcard gui/*.ui)
TS_FILES = $(wildcard i18n/*.ts) 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) UI_COMPILED = $(UI_FILES:.ui=.py)
TS_COMPILED = $(TS_FILES:.ts=.qm) TS_COMPILED = $(TS_FILES:.ts=.qm)

View File

@ -2,121 +2,33 @@
import sys import sys
import os.path import os.path
import re
import time import time
import tempfile import tempfile
import hashlib import hashlib
import zlib import zlib
import logging import logging
import serial
import serial.tools.list_ports
import requests import requests
from esptool import ESPLoader 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 from gui import mainwindow
# Firmware update repository from luftdatentool.consts import UPDATE_REPOSITORY, ALLOWED_PROTO, \
UPDATE_REPOSITORY = 'https://www.madavi.de/sensor/update/data/' PREFERED_PORTS, ROLE_DEVICE
# 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
if getattr(sys, 'frozen', False): if getattr(sys, 'frozen', False):
RESOURCES_PATH = sys._MEIPASS RESOURCES_PATH = sys._MEIPASS
else: else:
RESOURCES_PATH = os.path.dirname(os.path.realpath(__file__)) 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'<a href="([^"]*)">([^<]*)</a>')
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): class MainWindow(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
signal = QtCore.Signal([str, int]) uploadProgress = QtCore.Signal([str, int])
errorSignal = QtCore.Signal([str]) errorSignal = QtCore.Signal([str])
uploadThread = None uploadThread = None
@ -136,17 +48,20 @@ class MainWindow(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
self.i18n_init(QtCore.QLocale.system()) self.i18n_init(QtCore.QLocale.system())
self.statusbar.showMessage(self.tr("Loading firmware list...")) self.statusbar.showMessage(self.tr("Loading firmware list..."))
self.firmware_list = FirmwareListThread() 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.firmware_list.start()
self.port_detect = PortDetectThread() self.port_detect = PortDetectThread()
self.port_detect.portsUpdate.connect(self.populate_boards) self.port_detect.portsUpdate.connect(self.populate_boards)
self.port_detect.error.connect(self.on_work_error)
self.port_detect.start() self.port_detect.start()
self.on_expertModeBox_clicked() 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.errorSignal.connect(self.on_work_error)
self.cachedir = tempfile.TemporaryDirectory() self.cachedir = tempfile.TemporaryDirectory()
@ -257,7 +172,7 @@ class MainWindow(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
self.statusbar.showMessage(self.tr("Work in progess...")) self.statusbar.showMessage(self.tr("Work in progess..."))
return return
self.flash_board(self.signal, device, binary_uri, self.flash_board(self.uploadProgress, device, binary_uri,
error=self.errorSignal) error=self.errorSignal)
def cache_download(self, progress, binary_uri): def cache_download(self, progress, binary_uri):
@ -291,8 +206,6 @@ class MainWindow(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
if binary_uri.startswith(ALLOWED_PROTO): if binary_uri.startswith(ALLOWED_PROTO):
binary_uri = self.cache_download(progress, binary_uri) binary_uri = self.cache_download(progress, binary_uri)
print(binary_uri)
progress.emit(self.tr('Connecting...'), 0) progress.emit(self.tr('Connecting...'), 0)
init_baud = min(ESPLoader.ESP_ROM_BAUD, baudrate) init_baud = min(ESPLoader.ESP_ROM_BAUD, baudrate)

View File

18
luftdatentool/consts.py Normal file
View File

@ -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

View File

@ -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

56
luftdatentool/utils.py Normal file
View File

@ -0,0 +1,56 @@
import requests
import re
import logging
from .qtvariant import QtCore
file_index_re = re.compile(r'<a href="([^"]*)">([^<]*)</a>')
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

53
luftdatentool/workers.py Normal file
View File

@ -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)

View File

@ -6,6 +6,7 @@ esptool==2.5.0
future==0.16.0 future==0.16.0
idna==2.7 idna==2.7
macholib==1.11 macholib==1.11
netifaces==0.10.7
pefile==2018.8.8 pefile==2018.8.8
pyaes==1.6.1 pyaes==1.6.1
https://github.com/pyinstaller/pyinstaller/archive/bbf964c6b89ca33823031fa7ed277c0269192b3e.zip#egg=PyInstaller https://github.com/pyinstaller/pyinstaller/archive/bbf964c6b89ca33823031fa7ed277c0269192b3e.zip#egg=PyInstaller
@ -14,3 +15,4 @@ PyQt5-sip==4.19.12
pyserial==3.4 pyserial==3.4
requests==2.19.1 requests==2.19.1
urllib3==1.23 urllib3==1.23
zeroconf==0.20.0