Compare commits

..

5 Commits

12 changed files with 624 additions and 229 deletions

2
.gitignore vendored
View File

@ -2,6 +2,8 @@
*.py[oc] *.py[oc]
*.swp *.swp
_buildid.py
build/ build/
dist/ dist/

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

@ -6,8 +6,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>503</width> <width>493</width>
<height>530</height> <height>466</height>
</rect> </rect>
</property> </property>
<property name="sizePolicy"> <property name="sizePolicy">
@ -17,7 +17,7 @@
</sizepolicy> </sizepolicy>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
<string>Luftdaten.info Flashing Tool</string> <string>Luftdaten.info Flashing Tool (v{version})</string>
</property> </property>
<property name="windowIcon"> <property name="windowIcon">
<iconset> <iconset>
@ -32,39 +32,73 @@
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout"> <layout class="QVBoxLayout" name="verticalLayout">
<item> <item>
<widget class="QWidget" name="formWidget" native="true"> <widget class="QFrame" name="globalMessage">
<property name="sizePolicy"> <property name="frameShape">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum"> <enum>QFrame::StyledPanel</enum>
<horstretch>0</horstretch> </property>
<verstretch>0</verstretch> <property name="frameShadow">
</sizepolicy> <enum>QFrame::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="globalMessageTitle">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property> </property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text"> <property name="text">
<string>Board:</string> <string/>
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="1"> <item>
<widget class="QComboBox" name="boardBox"> <widget class="QLabel" name="globalMessageText">
<property name="text">
<string/>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="layoutDirection">
<enum>Qt::LeftToRight</enum>
</property>
<property name="tabPosition">
<enum>QTabWidget::South</enum>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="programmingTab">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed"> <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch> <horstretch>0</horstretch>
<verstretch>0</verstretch> <verstretch>0</verstretch>
</sizepolicy> </sizepolicy>
</property> </property>
</widget> <attribute name="title">
</item> <string>Flashing</string>
<item row="1" column="0"> </attribute>
<layout class="QGridLayout" name="gridLayout_2" rowstretch="0,0,0,0,0,0,0,1">
<item row="3" column="0">
<widget class="QLabel" name="label_2"> <widget class="QLabel" name="label_2">
<property name="text"> <property name="text">
<string>Firmware version:</string> <string>Firmware version:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="1"> <item row="5" column="0" colspan="2">
<widget class="QProgressBar" name="progressBar"/>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="versionBox"> <widget class="QComboBox" name="versionBox">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed"> <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
@ -77,7 +111,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="0" colspan="2"> <item row="6" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout"> <layout class="QHBoxLayout" name="horizontalLayout">
<item> <item>
<widget class="QPushButton" name="uploadButton"> <widget class="QPushButton" name="uploadButton">
@ -104,13 +138,24 @@
</item> </item>
</layout> </layout>
</item> </item>
<item row="2" column="0" colspan="2"> <item row="0" column="1">
<widget class="QProgressBar" name="progressBar"/> <widget class="QComboBox" name="boardBox">
</item> <property name="sizePolicy">
</layout> <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget> </widget>
</item> </item>
<item> <item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Board:</string>
</property>
</widget>
</item>
<item row="7" column="0" colspan="2">
<widget class="QWidget" name="expertForm" native="true"> <widget class="QWidget" name="expertForm" native="true">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding"> <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
@ -151,13 +196,153 @@
</item> </item>
</layout> </layout>
</widget> </widget>
<widget class="QWidget" name="discoveryTab">
<attribute name="title">
<string>Discovery</string>
</attribute>
<layout class="QGridLayout" name="gridLayout">
<item row="3" column="0" colspan="2">
<widget class="QListWidget" name="discoveryList"/>
</item>
<item row="4" column="0" colspan="2">
<widget class="QLabel" name="label_5">
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text">
<string>Double-click to open configuration page.</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_4">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Sensors detected in local network:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="discoveryRefreshButton">
<property name="text">
<string>Refresh</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="serialTab">
<attribute name="title">
<string>Serial Monitor</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_4">
<item row="2" column="0">
<widget class="QTextEdit" name="serialTextEdit"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
<string>In case of sensor issues, Serial Monitor can be used to review logs sent by the sensor over USB cable.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QPushButton" name="serialConnectButton">
<property name="text">
<string>Connect</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="aboutTab">
<attribute name="title">
<string>About</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_3">
<property name="spacing">
<number>12</number>
</property>
<item row="1" column="1">
<widget class="QLabel" name="buildLabel">
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>&lt;b&gt;Luftdaten.info Flashing Tool&lt;/b&gt;&lt;br/&gt;Build {build_id}</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_6">
<property name="maximumSize">
<size>
<width>64</width>
<height>64</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap>../assets/logo.png</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QLabel" name="label_8">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Developed by &lt;a href=&quot;https://inf.re/&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;Piotr Dobrowolski&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;This software is released under the terms of MIT license. No warranty is provided.&lt;/p&gt;&lt;p&gt;For newest release see: &lt;a href=&quot;https://d.inf.re/luftdaten/&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;https://d.inf.re/luftdaten/&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar"> <widget class="QMenuBar" name="menubar">
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>503</width> <width>493</width>
<height>22</height> <height>20</height>
</rect> </rect>
</property> </property>
</widget> </widget>

View File

@ -4,94 +4,149 @@
<context> <context>
<name>MainWindow</name> <name>MainWindow</name>
<message> <message>
<location filename="../luftdaten-tool.py" line="199"/> <location filename="../luftdaten-tool.py" line="132"/>
<source>No boards found</source> <source>No boards found</source>
<translation>Nie znaleziono płytki</translation> <translation>Nie znaleziono płytki</translation>
</message> </message>
<message> <message>
<location filename="../luftdaten-tool.py" line="204"/> <location filename="../luftdaten-tool.py" line="141"/>
<source>Others...</source> <source>Others...</source>
<translation>Inne...</translation> <translation>Inne...</translation>
</message> </message>
<message> <message>
<location filename="../luftdaten-tool.py" line="233"/> <location filename="../luftdaten-tool.py" line="170"/>
<source>No device selected.</source> <source>No device selected.</source>
<translation>Nie wybrano urządzenia.</translation> <translation>Nie wybrano urządzenia.</translation>
</message> </message>
<message> <message>
<location filename="../luftdaten-tool.py" line="237"/> <location filename="../luftdaten-tool.py" line="174"/>
<source>No version selected.</source> <source>No version selected.</source>
<translation>Nie wybrano wersji.</translation> <translation>Nie wybrano wersji.</translation>
</message> </message>
<message> <message>
<location filename="../gui/mainwindow.py" line="131"/> <location filename="../gui/mainwindow.py" line="226"/>
<source>Board:</source> <source>Board:</source>
<translation>Płytka:</translation> <translation>Płytka:</translation>
</message> </message>
<message> <message>
<location filename="../gui/mainwindow.py" line="132"/> <location filename="../gui/mainwindow.py" line="223"/>
<source>Firmware version:</source> <source>Firmware version:</source>
<translation>Wersja oprogramowania:</translation> <translation>Wersja oprogramowania:</translation>
</message> </message>
<message> <message>
<location filename="../gui/mainwindow.py" line="133"/> <location filename="../gui/mainwindow.py" line="224"/>
<source>Upload</source> <source>Upload</source>
<translation>Wgraj</translation> <translation>Wgraj</translation>
</message> </message>
<message> <message>
<location filename="../gui/mainwindow.py" line="134"/> <location filename="../gui/mainwindow.py" line="225"/>
<source>Expert mode</source> <source>Expert mode</source>
<translation>Tryb eksperta</translation> <translation>Tryb eksperta</translation>
</message> </message>
<message> <message>
<location filename="../gui/mainwindow.py" line="135"/> <location filename="../gui/mainwindow.py" line="227"/>
<source>Baudrate:</source> <source>Baudrate:</source>
<translation>Prędkość portu:</translation> <translation>Prędkość portu:</translation>
</message> </message>
<message> <message>
<location filename="../luftdaten-tool.py" line="252"/> <location filename="../luftdaten-tool.py" line="189"/>
<source>Invalid version / file does not exist</source> <source>Invalid version / file does not exist</source>
<translation>Błędna wersja / plik nie istnieje</translation> <translation>Błędna wersja / plik nie istnieje</translation>
</message> </message>
<message> <message>
<location filename="../luftdaten-tool.py" line="257"/> <location filename="../luftdaten-tool.py" line="194"/>
<source>Work in progess...</source> <source>Work in progess...</source>
<translation>Praca w toku...</translation> <translation>Praca w toku...</translation>
</message> </message>
<message> <message>
<location filename="../luftdaten-tool.py" line="284"/> <location filename="../luftdaten-tool.py" line="221"/>
<source>Downloading...</source> <source>Downloading...</source>
<translation>Pobieranie...</translation> <translation>Pobieranie...</translation>
</message> </message>
<message> <message>
<location filename="../luftdaten-tool.py" line="296"/> <location filename="../luftdaten-tool.py" line="231"/>
<source>Connecting...</source> <source>Connecting...</source>
<translation>Łączenie...</translation> <translation>Łączenie...</translation>
</message> </message>
<message> <message>
<location filename="../luftdaten-tool.py" line="301"/> <location filename="../luftdaten-tool.py" line="236"/>
<source>Connected. Chip type: {chip_type}</source> <source>Connected. Chip type: {chip_type}</source>
<translation>Połączono. Typ układu: {chip_type}</translation> <translation>Połączono. Typ układu: {chip_type}</translation>
</message> </message>
<message> <message>
<location filename="../luftdaten-tool.py" line="319"/> <location filename="../luftdaten-tool.py" line="254"/>
<source>Writing at 0x{address:08x}...</source> <source>Writing at 0x{address:08x}...</source>
<translation>Zapisywanie pod adresem 0x{address:08x}...</translation> <translation>Zapisywanie pod adresem 0x{address:08x}...</translation>
</message> </message>
<message> <message>
<location filename="../luftdaten-tool.py" line="330"/> <location filename="../luftdaten-tool.py" line="265"/>
<source>Finished in {time:.2f} seconds. Sensor ID: {sensor_id}</source> <source>Finished in {time:.2f} seconds. Sensor ID: {sensor_id}</source>
<translation>Zakończono w {time:.2f} sekundy. ID sensora: {sensor_id}</translation> <translation>Zakończono w {time:.2f} sekundy. ID sensora: {sensor_id}</translation>
</message> </message>
<message> <message>
<location filename="../gui/mainwindow.py" line="130"/> <location filename="../gui/mainwindow.py" line="236"/>
<source>Luftdaten.info Flashing Tool</source> <source>Luftdaten.info Flashing Tool</source>
<translation>Luftdaten.info Flashing Tool</translation> <translation>Luftdaten.info Flashing Tool</translation>
</message> </message>
<message> <message>
<location filename="../luftdaten-tool.py" line="138"/> <location filename="../luftdaten-tool.py" line="52"/>
<source>Loading firmware list...</source> <source>Loading firmware list...</source>
<translation>Ładowanie wersji oprogramowania...</translation> <translation>Ładowanie wersji oprogramowania...</translation>
</message> </message>
<message>
<location filename="../luftdaten-tool.py" line="132"/>
<source>Have you installed &lt;a href=&quot;{drivers_url}&quot;&gt;the drivers&lt;/a&gt;?</source>
<translation>Czy zainstalowałeś &lt;a href=&quot;{drivers_url}&quot;&gt;sterowniki&lt;/a&gt;?</translation>
</message>
<message>
<location filename="../gui/mainwindow.py" line="228"/>
<source>Flashing</source>
<translation>Programowanie</translation>
</message>
<message>
<location filename="../gui/mainwindow.py" line="229"/>
<source>Double-click to open configuration page.</source>
<translation>Naciśnij dwukrotnie aby otworzyć stronę konfiguracji.</translation>
</message>
<message>
<location filename="../gui/mainwindow.py" line="230"/>
<source>Sensors detected in local network:</source>
<translation>Sensory wykryte w sieci lokalnej:</translation>
</message>
<message>
<location filename="../gui/mainwindow.py" line="231"/>
<source>Refresh</source>
<translation>Odśwież</translation>
</message>
<message>
<location filename="../gui/mainwindow.py" line="232"/>
<source>Discovery</source>
<translation>Wykrywanie</translation>
</message>
<message>
<location filename="../gui/mainwindow.py" line="233"/>
<source>In case of sensor issues, Serial Monitor can be used to review logs sent by the sensor over USB cable.</source>
<translation>W przypadku problemów z sensorem, Serial Monitor pozwala na przejrzenie stanu urządzenia przez kabel USB.</translation>
</message>
<message>
<location filename="../gui/mainwindow.py" line="234"/>
<source>Connect</source>
<translation>Połącz</translation>
</message>
<message>
<location filename="../gui/mainwindow.py" line="235"/>
<source>Serial Monitor</source>
<translation>Serial Monitor</translation>
</message>
<message>
<location filename="../gui/mainwindow.py" line="237"/>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Developed by &lt;a href=&quot;https://inf.re/&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;Piotr Dobrowolski&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;This software is released under the terms of MIT license. No warranty is provided.&lt;/p&gt;&lt;p&gt;For newest release see: &lt;a href=&quot;https://d.inf.re/luftdaten/&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;https://d.inf.re/luftdaten/&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Wykonane przez &lt;a href=&quot;https://inf.re/&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;Piotr Dobrowolski&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Ten program wypuszczony jest na warunkach licencji MIT. Brak gwarancji.&lt;/p&gt;&lt;p&gt;Najnowszą wersję znajdziesz na: &lt;a href=&quot;https://d.inf.re/luftdaten/&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;https://d.inf.re/luftdaten/&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<location filename="../gui/mainwindow.py" line="238"/>
<source>About</source>
<translation>O programie</translation>
</message>
</context> </context>
</TS> </TS>

View File

@ -2,123 +2,38 @@
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
import luftdatentool
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, DRIVERS_URL
# 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
zeroconf_discovery = None
boards_detected = False
def __init__(self, parent=None, app=None): def __init__(self, parent=None, app=None):
super(MainWindow, self).__init__(parent) super(MainWindow, self).__init__(parent)
@ -131,26 +46,41 @@ class MainWindow(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
os.chdir(oldcwd) os.chdir(oldcwd)
self.app = app self.app = app
self.translator = QtCore.QTranslator()
self.translator = QtCore.QTranslator()
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.discovery_start()
self.signal.connect(self.on_work_update) self.globalMessage.hide()
# Hide WIP GUI parts...
self.on_expertModeBox_clicked()
self.expertModeBox.hide()
self.tabWidget.removeTab(self.tabWidget.indexOf(self.serialTab))
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()
def show_global_message(self, title, message):
self.globalMessage.show()
self.globalMessageTitle.setText(title)
self.globalMessageText.setText(message)
def on_work_update(self, status, progress): def on_work_update(self, status, progress):
self.statusbar.showMessage(status) self.statusbar.showMessage(status)
self.progressBar.setValue(progress) self.progressBar.setValue(progress)
@ -158,6 +88,21 @@ class MainWindow(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
def on_work_error(self, message): def on_work_error(self, message):
self.statusbar.showMessage(message) self.statusbar.showMessage(message)
@property
def version(self):
return luftdatentool.__version__
@property
def build_id(self):
try:
from luftdatentool._buildid import commit, builddate
except ImportError:
import datetime
commit = 'devel'
builddate = datetime.datetime.now().strftime('%Y%m%d')
return '{}-{}/{}'.format(self.version, commit, builddate)
def i18n_init(self, locale): def i18n_init(self, locale):
"""Initializes i18n to specified QLocale""" """Initializes i18n to specified QLocale"""
@ -168,6 +113,13 @@ class MainWindow(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
self.app.installTranslator(self.translator) self.app.installTranslator(self.translator)
self.retranslateUi(self) self.retranslateUi(self)
def retranslateUi(self, win):
super(MainWindow, self).retranslateUi(win)
win.setWindowTitle(win.windowTitle().format(
version=self.version))
win.buildLabel.setText(win.buildLabel.text().format(
build_id=self.build_id))
def populate_versions(self, files): def populate_versions(self, files):
"""Loads available firmware versions into versionbox widget""" """Loads available firmware versions into versionbox widget"""
@ -200,6 +152,17 @@ class MainWindow(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
sep.setEnabled(False) sep.setEnabled(False)
self.boardBox.model().appendRow(sep) self.boardBox.model().appendRow(sep)
# No prefered boards has been found so far and there is a
# suggested driver download URL available
if not self.boards_detected and DRIVERS_URL:
self.show_global_message(
self.tr('No boards found'),
self.tr('Have you installed <a href="{drivers_url}">'
'the drivers</a>?').format(drivers_url=DRIVERS_URL))
else:
self.globalMessage.hide()
self.boards_detected = True
if others: if others:
sep = QtGui.QStandardItem(self.tr('Others...')) sep = QtGui.QStandardItem(self.tr('Others...'))
sep.setEnabled(False) sep.setEnabled(False)
@ -257,7 +220,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 +254,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)
@ -334,9 +295,36 @@ class MainWindow(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
@QtCore.Slot() @QtCore.Slot()
def on_expertModeBox_clicked(self): def on_expertModeBox_clicked(self):
self.expertForm.setVisible(self.expertModeBox.checkState()) self.expertForm.setVisible(self.expertModeBox.checkState())
self.centralwidget.setFixedHeight( #self.centralwidget.setFixedHeight(
self.centralwidget.sizeHint().height()) # self.centralwidget.sizeHint().height())
self.setFixedHeight(self.sizeHint().height()) #self.setFixedHeight(self.sizeHint().height())
# Zeroconf page
def discovery_start(self):
if self.zeroconf_discovery:
self.zeroconf_discovery.stop()
self.zeroconf_discovery = ZeroconfDiscoveryThread()
self.zeroconf_discovery.deviceDiscovered.connect(
self.on_zeroconf_discovered)
self.zeroconf_discovery.start()
def on_zeroconf_discovered(self, name, address, info):
"""Called on every zeroconf discovered device"""
if name.startswith('Feinstaubsensor'):
item = QtWidgets.QListWidgetItem('{}: {}'.format(address, name.split('.')[0]))
item.setData(ROLE_DEVICE, 'http://{}:{}'.format(address, info.port))
self.discoveryList.addItem(item)
@QtCore.Slot(QtWidgets.QListWidgetItem)
def on_discoveryList_itemDoubleClicked(self, index):
QtGui.QDesktopServices.openUrl(QtCore.QUrl(index.data(ROLE_DEVICE)))
@QtCore.Slot()
def on_discoveryRefreshButton_clicked(self):
self.discoveryList.clear()
self.discovery_start()
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -2,6 +2,16 @@
block_cipher = None block_cipher = None
import subprocess
import datetime
commit = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).strip().decode('utf-8')
builddate = datetime.datetime.now().strftime('%Y%m%d')
with open('luftdatentool/_buildid.py', 'w') as fd:
fd.write('''# This file is autogenerated in luftdaten-tool.spec file
commit = "{commit}"
builddate = "{builddate}"'''.format(commit=commit, builddate=builddate))
a = Analysis(['luftdaten-tool.py'], a = Analysis(['luftdaten-tool.py'],
pathex=['.'], pathex=['.'],

View File

@ -0,0 +1 @@
__version__ = '0.2'

28
luftdatentool/consts.py Normal file
View File

@ -0,0 +1,28 @@
import sys
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
if sys.platform.startswith('darwin'):
DRIVERS_URL = 'http://www.wch.cn/downloads/CH341SER_MAC_ZIP.html'
elif sys.platform.startswith(('cygwin', 'win32')):
DRIVERS_URL = 'http://www.wch.cn/downloads/CH341SER_ZIP.html'
else:
DRIVERS_URL = None

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

60
luftdatentool/workers.py Normal file
View File

@ -0,0 +1,60 @@
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 = None
while True:
new_ports = serial.tools.list_ports.comports()
if ports is None or [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)
browser = None
def target(self):
"""This thread scans for Bonjour/mDNS devices and emits
deviceDiscovered signal with its name, address and info object"""
self.zc = zeroconf.Zeroconf()
self.browser = zeroconf.ServiceBrowser(self.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)
def stop(self):
if self.browser:
self.browser.cancel()

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