Initial working version
This commit is contained in:
parent
accfb19626
commit
edd1372938
@ -93,6 +93,14 @@
|
|||||||
<verstretch>0</verstretch>
|
<verstretch>0</verstretch>
|
||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<pointsize>11</pointsize>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="styleSheet">
|
||||||
|
<string notr="true">padding:10px</string>
|
||||||
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Upload</string>
|
<string>Upload</string>
|
||||||
</property>
|
</property>
|
||||||
|
@ -1,61 +1,97 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!DOCTYPE TS><TS version="2.0" language="pl" sourcelanguage="">
|
<!DOCTYPE TS>
|
||||||
|
<TS version="2.1" language="pl">
|
||||||
<context>
|
<context>
|
||||||
<name>MainWindow</name>
|
<name>MainWindow</name>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../luftdaten-tool.py" line="59"/>
|
<location filename="../luftdaten-tool.py" line="138"/>
|
||||||
<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="64"/>
|
<location filename="../luftdaten-tool.py" line="143"/>
|
||||||
<source>Others...</source>
|
<source>Others...</source>
|
||||||
<translation>Inne...</translation>
|
<translation>Inne...</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../luftdaten-tool.py" line="96"/>
|
<location filename="../luftdaten-tool.py" line="172"/>
|
||||||
<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="100"/>
|
<location filename="../luftdaten-tool.py" line="176"/>
|
||||||
<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="126"/>
|
<location filename="../gui/mainwindow.py" line="131"/>
|
||||||
<source>Luftdaten.info Flashing Tool</source>
|
<source>Luftdaten.info Flashing Tool</source>
|
||||||
<translation></translation>
|
<translation></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../gui/mainwindow.py" line="127"/>
|
<location filename="../gui/mainwindow.py" line="132"/>
|
||||||
<source>Board:</source>
|
<source>Board:</source>
|
||||||
<translation>Płytka:</translation>
|
<translation>Płytka:</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../gui/mainwindow.py" line="128"/>
|
<location filename="../gui/mainwindow.py" line="133"/>
|
||||||
<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="129"/>
|
<location filename="../gui/mainwindow.py" line="134"/>
|
||||||
<source>master</source>
|
<source>master</source>
|
||||||
<translation>master</translation>
|
<translation>master</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../gui/mainwindow.py" line="130"/>
|
<location filename="../gui/mainwindow.py" line="135"/>
|
||||||
<source>Upload</source>
|
<source>Upload</source>
|
||||||
<translation>Wgraj</translation>
|
<translation>Wgraj</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../gui/mainwindow.py" line="131"/>
|
<location filename="../gui/mainwindow.py" line="136"/>
|
||||||
<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="132"/>
|
<location filename="../gui/mainwindow.py" line="137"/>
|
||||||
<source>Baudrate:</source>
|
<source>Baudrate:</source>
|
||||||
<translation>Prędkość portu:</translation>
|
<translation>Prędkość portu:</translation>
|
||||||
</message>
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../luftdaten-tool.py" line="191"/>
|
||||||
|
<source>Invalid version / file does not exist</source>
|
||||||
|
<translation>Błędna wersja / plik nie istnieje</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../luftdaten-tool.py" line="196"/>
|
||||||
|
<source>Work in progess...</source>
|
||||||
|
<translation>Praca w toku...</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../luftdaten-tool.py" line="224"/>
|
||||||
|
<source>Downloading...</source>
|
||||||
|
<translation>Pobieranie...</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../luftdaten-tool.py" line="235"/>
|
||||||
|
<source>Connecting...</source>
|
||||||
|
<translation>Łączenie...</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../luftdaten-tool.py" line="240"/>
|
||||||
|
<source>Done. Chip type: %s</source>
|
||||||
|
<translation>Zrobione. Typ układu: %s</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../luftdaten-tool.py" line="258"/>
|
||||||
|
<source>Writing at 0x%08x...</source>
|
||||||
|
<translation>Zapisywanie pod adresem 0x%08x...</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../luftdaten-tool.py" line="268"/>
|
||||||
|
<source>Finished in %.2f seconds</source>
|
||||||
|
<translation>Zrobione w %.2f sekundy</translation>
|
||||||
|
</message>
|
||||||
</context>
|
</context>
|
||||||
</TS>
|
</TS>
|
||||||
|
@ -1,14 +1,27 @@
|
|||||||
# -* encoding: utf-8 *-
|
# -* encoding: utf-8 *-
|
||||||
import sys
|
import sys
|
||||||
import os.path
|
import os.path
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import tempfile
|
||||||
|
import hashlib
|
||||||
|
import zlib
|
||||||
|
|
||||||
import serial
|
import serial
|
||||||
import serial.tools.list_ports
|
import serial.tools.list_ports
|
||||||
|
import requests
|
||||||
|
from esptool import ESPLoader
|
||||||
from PyQt5 import QtGui, QtCore, QtWidgets
|
from PyQt5 import QtGui, QtCore, QtWidgets
|
||||||
|
|
||||||
from gui import mainwindow
|
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 = [
|
PREFERED_PORTS = [
|
||||||
# CH341
|
# CH341
|
||||||
(0x1A86, 0x7523),
|
(0x1A86, 0x7523),
|
||||||
@ -19,8 +32,44 @@ PREFERED_PORTS = [
|
|||||||
|
|
||||||
ROLE_DEVICE = QtCore.Qt.UserRole + 1
|
ROLE_DEVICE = QtCore.Qt.UserRole + 1
|
||||||
|
|
||||||
|
# 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 QThread(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(QThread, 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
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
class MainWindow(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
||||||
|
signal = QtCore.Signal([str, int])
|
||||||
|
errorSignal = QtCore.Signal([str])
|
||||||
|
uploadThread = None
|
||||||
|
|
||||||
def __init__(self, parent=None, app=None):
|
def __init__(self, parent=None, app=None):
|
||||||
super(MainWindow, self).__init__(parent)
|
super(MainWindow, self).__init__(parent)
|
||||||
|
|
||||||
@ -34,18 +83,50 @@ class MainWindow(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
|||||||
self.translator = QtCore.QTranslator()
|
self.translator = QtCore.QTranslator()
|
||||||
|
|
||||||
self.i18n_init(QtCore.QLocale.system())
|
self.i18n_init(QtCore.QLocale.system())
|
||||||
|
|
||||||
|
# TODO: extract this to separate thread
|
||||||
|
self.populate_versions()
|
||||||
self.populate_boards(serial.tools.list_ports.comports())
|
self.populate_boards(serial.tools.list_ports.comports())
|
||||||
|
|
||||||
self.on_expertModeBox_clicked()
|
self.on_expertModeBox_clicked()
|
||||||
|
|
||||||
|
self.signal.connect(self.on_work_update)
|
||||||
|
self.errorSignal.connect(self.on_work_error)
|
||||||
|
|
||||||
|
self.cachedir = tempfile.TemporaryDirectory()
|
||||||
|
|
||||||
|
def on_work_update(self, status, progress):
|
||||||
|
self.statusbar.showMessage(status)
|
||||||
|
self.progressBar.setValue(progress)
|
||||||
|
|
||||||
|
def on_work_error(self, message):
|
||||||
|
self.statusbar.showMessage(message)
|
||||||
|
|
||||||
def i18n_init(self, locale):
|
def i18n_init(self, locale):
|
||||||
|
"""Initializes i18n to specified QLocale"""
|
||||||
|
|
||||||
self.app.removeTranslator(self.translator)
|
self.app.removeTranslator(self.translator)
|
||||||
self.translator.load(os.path.join('i18n', QtCore.QLocale.languageToString(locale.language())+ '.qm'))
|
lang = QtCore.QLocale.languageToString(locale.language())
|
||||||
|
self.translator.load(os.path.join('i18n', lang + '.qm'))
|
||||||
self.app.installTranslator(self.translator)
|
self.app.installTranslator(self.translator)
|
||||||
self.retranslateUi(self)
|
self.retranslateUi(self)
|
||||||
|
|
||||||
|
def populate_versions(self):
|
||||||
|
"""Loads available firmware versions into versionbox widget"""
|
||||||
|
|
||||||
|
for fname in indexof(UPDATE_REPOSITORY):
|
||||||
|
if not fname.endswith('.bin'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
item = QtGui.QStandardItem(fname)
|
||||||
|
item.setData(UPDATE_REPOSITORY + fname, ROLE_DEVICE)
|
||||||
|
self.versionBox.model().appendRow(item)
|
||||||
|
|
||||||
def populate_boards(self, ports):
|
def populate_boards(self, ports):
|
||||||
prefered, others = self.group_ports(serial.tools.list_ports.comports())
|
"""Populates board selection combobox from list of pyserial
|
||||||
|
ListPortInfo objects"""
|
||||||
|
|
||||||
|
prefered, others = self.group_ports(ports)
|
||||||
|
|
||||||
for b in prefered:
|
for b in prefered:
|
||||||
item = QtGui.QStandardItem(
|
item = QtGui.QStandardItem(
|
||||||
@ -80,10 +161,7 @@ class MainWindow(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
|||||||
others.append(p)
|
others.append(p)
|
||||||
return prefered, others
|
return prefered, others
|
||||||
|
|
||||||
def on_actionExit_triggered(self):
|
@QtCore.Slot()
|
||||||
"""This handles activation of "Exit" menu action"""
|
|
||||||
self.app.exit()
|
|
||||||
|
|
||||||
def on_uploadButton_clicked(self):
|
def on_uploadButton_clicked(self):
|
||||||
self.statusbar.clearMessage()
|
self.statusbar.clearMessage()
|
||||||
|
|
||||||
@ -98,6 +176,98 @@ class MainWindow(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
|
|||||||
self.statusbar.showMessage(self.tr("No version selected."))
|
self.statusbar.showMessage(self.tr("No version selected."))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
orig_version = self.versionBox.model().item(
|
||||||
|
self.versionBox.currentIndex()).text()
|
||||||
|
|
||||||
|
if version == orig_version:
|
||||||
|
# Editable combobox has been unchanged
|
||||||
|
binary_uri = self.versionBox.currentData(ROLE_DEVICE)
|
||||||
|
elif version.startswith(ALLOWED_PROTO):
|
||||||
|
# User has provided a download URL
|
||||||
|
binary_uri = version
|
||||||
|
elif os.path.exists(version):
|
||||||
|
binary_uri = version
|
||||||
|
else:
|
||||||
|
self.statusbar.showMessage(self.tr(
|
||||||
|
"Invalid version / file does not exist"))
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.uploadThread and self.uploadThread.isRunning():
|
||||||
|
self.statusbar.showMessage(self.tr("Work in progess..."))
|
||||||
|
return
|
||||||
|
|
||||||
|
self.uploadThread = QThread(self, self.flash_board, [
|
||||||
|
self.signal, device, binary_uri], error=self.errorSignal)
|
||||||
|
self.uploadThread.start()
|
||||||
|
|
||||||
|
def cache_download(self, progress, binary_uri):
|
||||||
|
"""Downloads and caches file with status reports via Qt Signals"""
|
||||||
|
cache_fname = os.path.join(
|
||||||
|
self.cachedir.name,
|
||||||
|
hashlib.sha256(binary_uri.encode('utf-8')).hexdigest())
|
||||||
|
|
||||||
|
if os.path.exists(cache_fname):
|
||||||
|
return cache_fname
|
||||||
|
|
||||||
|
with open(cache_fname, 'wb') as fd:
|
||||||
|
progress.emit(self.tr('Downloading...'), 0)
|
||||||
|
response = requests.get(binary_uri, stream=True)
|
||||||
|
total_length = response.headers.get('content-length')
|
||||||
|
|
||||||
|
dl = 0
|
||||||
|
total_length = int(total_length or 0)
|
||||||
|
for data in response.iter_content(chunk_size=4096):
|
||||||
|
dl += len(data)
|
||||||
|
fd.write(data)
|
||||||
|
|
||||||
|
if total_length:
|
||||||
|
progress.emit(self.tr('Downloading...'),
|
||||||
|
(100*dl) // total_length)
|
||||||
|
|
||||||
|
return cache_fname
|
||||||
|
|
||||||
|
def flash_board(self, progress, device, binary_uri, baudrate=460800):
|
||||||
|
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)
|
||||||
|
esp = ESPLoader.detect_chip(device, init_baud, 'default_reset', False)
|
||||||
|
|
||||||
|
progress.emit(self.tr('Done. Chip type: %s') %
|
||||||
|
esp.get_chip_description(), 0)
|
||||||
|
esp = esp.run_stub()
|
||||||
|
esp.change_baud(baudrate)
|
||||||
|
|
||||||
|
with open(binary_uri, 'rb') as fd:
|
||||||
|
uncimage = fd.read()
|
||||||
|
|
||||||
|
image = zlib.compress(uncimage, 9)
|
||||||
|
|
||||||
|
address = 0x0
|
||||||
|
blocks = esp.flash_defl_begin(len(uncimage), len(image), address)
|
||||||
|
|
||||||
|
seq = 0
|
||||||
|
written = 0
|
||||||
|
t = time.time()
|
||||||
|
while len(image) > 0:
|
||||||
|
current_addr = address + seq * esp.FLASH_WRITE_SIZE
|
||||||
|
progress.emit(self.tr('Writing at 0x%08x...') % (current_addr,),
|
||||||
|
100 * (seq + 1) // blocks)
|
||||||
|
|
||||||
|
block = image[0:esp.FLASH_WRITE_SIZE]
|
||||||
|
esp.flash_defl_block(block, seq, timeout=3.0)
|
||||||
|
image = image[esp.FLASH_WRITE_SIZE:]
|
||||||
|
seq += 1
|
||||||
|
written += len(block)
|
||||||
|
t = time.time() - t
|
||||||
|
|
||||||
|
progress.emit(self.tr('Finished in %.2f seconds') % (t,), 100)
|
||||||
|
|
||||||
|
@QtCore.Slot()
|
||||||
def on_expertModeBox_clicked(self):
|
def on_expertModeBox_clicked(self):
|
||||||
self.expertForm.setVisible(self.expertModeBox.checkState())
|
self.expertForm.setVisible(self.expertModeBox.checkState())
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user