Compare commits

...

16 Commits

Author SHA1 Message Date
ecac90373c Add build identifier and initial version bump 2018-09-13 12:07:04 +02:00
1e46161c6a Show "no boards found" combo item, even if no ports are found 2018-09-13 12:07:04 +02:00
a26852bf14 GUI revamp 2018-09-10 16:21:51 +02:00
f1023ebacb Zeroconf UI 2018-09-10 14:46:42 +02:00
1042835685 Initial refactor, zeroconf tests 2018-09-10 13:40:38 +02:00
af335bac94 Use GridLayout instead of FormLayout to fix sizing issues on MacOS 2018-09-04 14:54:37 +02:00
e12f27c1e1 Add loading firmware list message, fix windows-build.bat path 2018-09-04 12:44:51 +02:00
6031cf8213 Add port hotplug and firmware list download from separate thread 2018-09-04 09:22:56 +02:00
68a264f9ba Add dmg building on MacOS, move build-related stuff to deploy/ 2018-09-04 09:00:41 +02:00
e4a6983ed9 Add LICENSE 2018-09-02 14:57:18 +02:00
f947a382b7 Make dist non-interactive by default 2018-09-02 13:47:51 +02:00
9a426f25bb More build notes 2018-09-02 13:43:37 +02:00
e302b796ae MacOS build support 2018-09-02 12:55:48 +02:00
caf09bc432 Clear up status messages, add sensor ID display 2018-08-30 12:32:54 +02:00
12988acda9 Load assets and i18n into pyinstaller bundle 2018-08-28 22:04:06 +02:00
c734a7f5e7 Test pyinstaller spec file 2018-08-28 19:25:11 +02:00
19 changed files with 1005 additions and 225 deletions

5
.gitignore vendored
View File

@@ -2,6 +2,11 @@
*.py[oc]
*.swp
_buildid.py
build/
dist/
# Ignore compiled Qt assets
*.qm
gui/*.py

20
LICENSE Normal file
View File

@@ -0,0 +1,20 @@
MIT License
Copyright 2018 Piotr "inf" Dobrowolski <informatic@hackerspace.pl>
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,10 +1,17 @@
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)
# python3.6 on Windows is only available as python.exe
ifeq (, $(shell which python3))
PY ?= python
else
PY ?= python3
endif
%.py: %.ui
pyuic5 $< > $@
@@ -18,9 +25,28 @@ clean:
rm $(TS_COMPILED)
run: all
python3 luftdaten-tool.py
$(PY) luftdaten-tool.py
i18n-update:
# Updates all translation files in i18n/ directory
i18n-update: $(UI_COMPILED)
@for f in $(TS_FILES) ; do \
pylupdate5 *.py gui/*.py -ts $$f -verbose; \
pylupdate5 $(PY_FILES) -ts $$f -verbose; \
done
deps:
$(PY) -m pip install -U -r requirements.txt
# Here go platform-specific buildsteps
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Darwin)
PLATFORM_DEPS := assets/logo.icns
assets/logo.icns: assets/logo.png
deploy/mkicns $<
endif
dist: all $(PLATFORM_DEPS)
$(PY) -m PyInstaller -y luftdaten-tool.spec
dmg: dist
dmgbuild -s deploy/dmgbuild_settings.py -D app=dist/Luftdaten.info\ Flashing\ Tool.app "Luftdaten.info Flashing Tool" dist/luftdaten-tool.dmg

View File

@@ -7,6 +7,35 @@ Binary builds
Our main target is having working prebuilt binaries for users to simply
download and run, to avoid all the setup below.
### Linux
Currently Linux builds require *Python 3.6* (but 3.7 seems to work fine as
well), GNU make and Qt Linguist tools. Following packages should suffice on
Ubuntu:
sudo apt install qttools5-dev-tools python3.6 make
Then, to install python dependencies and build the binary use:
make deps dist
### Windows
Currently Windows builds require *Python 3.6* installed system-wide and added to
`%PATH%`.
To install python and cygwin dependencies and build everything use
`deploy\windows-build.bat` batch script.
### MacOS
Currently MacOS builds require *Python 3.6* and Qt SDK installed (just the "Qt >
5... > macOS" part in installer) with following added to $PATH:
export PATH="$HOME/Qt/5.11.1/clang_64/bin:$PATH"
Then just install dependencies and build everything using:
make deps dmg
Development
-----------

BIN
assets/logo.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 18 KiB

174
deploy/dmgbuild_settings.py Normal file
View File

@@ -0,0 +1,174 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import biplist
import os.path
#
# Example settings file for dmgbuild
#
# Use like this: dmgbuild -s settings.py "Test Volume" test.dmg
# You can actually use this file for your own application (not just TextEdit)
# by doing e.g.
#
# dmgbuild -s settings.py -D app=/path/to/My.app "My Application" MyApp.dmg
# .. Useful stuff ..............................................................
application = defines.get('app', '/Applications/TextEdit.app')
appname = os.path.basename(application)
def icon_from_app(app_path):
plist_path = os.path.join(app_path, 'Contents', 'Info.plist')
plist = biplist.readPlist(plist_path)
icon_name = plist['CFBundleIconFile']
icon_root,icon_ext = os.path.splitext(icon_name)
if not icon_ext:
icon_ext = '.icns'
icon_name = icon_root + icon_ext
return os.path.join(app_path, 'Contents', 'Resources', icon_name)
# .. Basics ....................................................................
# Uncomment to override the output filename
# filename = 'test.dmg'
# Uncomment to override the output volume name
# volume_name = 'Test'
# Volume format (see hdiutil create -help)
format = defines.get('format', 'UDBZ')
# Volume size (must be large enough for your files)
size = defines.get('size', '100M')
# Files to include
files = [ application ]
# Symlinks to create
symlinks = { 'Applications': '/Applications' }
# Volume icon
#
# You can either define icon, in which case that icon file will be copied to the
# image, *or* you can define badge_icon, in which case the icon file you specify
# will be used to badge the system's Removable Disk icon
#
#icon = '/path/to/icon.icns'
badge_icon = icon_from_app(application)
# Where to put the icons
icon_locations = {
appname: (140, 120),
'Applications': (500, 120)
}
# .. Window configuration ......................................................
# Background
#
# This is a STRING containing any of the following:
#
# #3344ff - web-style RGB color
# #34f - web-style RGB color, short form (#34f == #3344ff)
# rgb(1,0,0) - RGB color, each value is between 0 and 1
# hsl(120,1,.5) - HSL (hue saturation lightness) color
# hwb(300,0,0) - HWB (hue whiteness blackness) color
# cmyk(0,1,0,0) - CMYK color
# goldenrod - X11/SVG named color
# builtin-arrow - A simple built-in background with a blue arrow
# /foo/bar/baz.png - The path to an image file
#
# The hue component in hsl() and hwb() may include a unit; it defaults to
# degrees ('deg'), but also supports radians ('rad') and gradians ('grad'
# or 'gon').
#
# Other color components may be expressed either in the range 0 to 1, or
# as percentages (e.g. 60% is equivalent to 0.6).
background = 'builtin-arrow'
show_status_bar = False
show_tab_view = False
show_toolbar = False
show_pathbar = False
show_sidebar = False
sidebar_width = 180
# Window position in ((x, y), (w, h)) format
window_rect = ((100, 100), (640, 280))
# Select the default view; must be one of
#
# 'icon-view'
# 'list-view'
# 'column-view'
# 'coverflow'
#
default_view = 'icon-view'
# General view configuration
show_icon_preview = False
# Set these to True to force inclusion of icon/list view settings (otherwise
# we only include settings for the default view)
include_icon_view_settings = 'auto'
include_list_view_settings = 'auto'
# .. Icon view configuration ...................................................
arrange_by = None
grid_offset = (0, 0)
grid_spacing = 100
scroll_position = (0, 0)
label_pos = 'bottom' # or 'right'
text_size = 16
icon_size = 128
# .. List view configuration ...................................................
# Column names are as follows:
#
# name
# date-modified
# date-created
# date-added
# date-last-opened
# size
# kind
# label
# version
# comments
#
list_icon_size = 16
list_text_size = 12
list_scroll_position = (0, 0)
list_sort_by = 'name'
list_use_relative_dates = True
list_calculate_all_sizes = False,
list_columns = ('name', 'date-modified', 'size', 'kind', 'date-added')
list_column_widths = {
'name': 300,
'date-modified': 181,
'date-created': 181,
'date-added': 181,
'date-last-opened': 181,
'size': 97,
'kind': 115,
'label': 100,
'version': 75,
'comments': 300,
}
list_column_sort_directions = {
'name': 'ascending',
'date-modified': 'descending',
'date-created': 'descending',
'date-added': 'descending',
'date-last-opened': 'descending',
'size': 'descending',
'kind': 'ascending',
'label': 'ascending',
'version': 'ascending',
'comments': 'ascending',
}

20
deploy/mkicns Executable file
View File

@@ -0,0 +1,20 @@
#!/bin/sh
#
# Builds .icns file from a single .png file
#
set -e
filename="${1%.*}"
mkdir "$filename".iconset
for i in 16 32 128 256 ; do
n=$(( i * 2 ))
sips -z $i $i "$1" --out "$filename".iconset/icon_${i}x${i}.png
sips -z $n $n "$1" --out "$filename".iconset/icon_${i}x${i}@2x.png
[[ $n -eq 512 ]] && \
sips -z $n $n "$1" --out "$filename".iconset/icon_${n}x${n}.png
(( i++ ))
done
cp "$1" "$filename".iconset/icon_512x512@2x.png
iconutil -c icns "$filename".iconset
rm -r "$filename".iconset

21
deploy/windows-build.bat Normal file
View File

@@ -0,0 +1,21 @@
cd %~dp0\..
if not exist build mkdir build
rem Download cygwin installer
if not exist build\cygwin-x86.exe powershell -Command "Invoke-WebRequest https://cygwin.com/setup-x86.exe -OutFile build\cygwin-x86.exe"
rem Install required Cygwin packages
if not exist build\cygwin build\cygwin-x86.exe --site http://cygwin.mirror.constant.com ^
--no-shortcuts ^
--no-desktop ^
--quiet-mode ^
--root "%cd%\build\cygwin" ^
--arch x86 ^
--local-package-dir "%cd%\build\cygwin-packages" ^
--verbose ^
--prune-install ^
--no-admin ^
--packages qt5-linguist-tools,make
build\cygwin\bin\bash.exe --login -i -c "ln -s `which lrelease-qt5` /usr/bin/lrelease ; cd \"%cd%\" && make deps dist"

View File

@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>503</width>
<height>530</height>
<width>493</width>
<height>466</height>
</rect>
</property>
<property name="sizePolicy">
@@ -17,7 +17,7 @@
</sizepolicy>
</property>
<property name="windowTitle">
<string>Luftdaten.info Flashing Tool</string>
<string>Luftdaten.info Flashing Tool (v{version})</string>
</property>
<property name="windowIcon">
<iconset>
@@ -32,132 +32,306 @@
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QWidget" name="formWidget" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
<widget class="QFrame" name="globalMessage">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="frameShadow">
<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 name="text">
<string>Board:</string>
<string/>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="boardBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<item>
<widget class="QLabel" name="globalMessageText">
<property name="text">
<string>Firmware version:</string>
<string/>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="versionBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="editable">
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="currentText">
<string>master</string>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QProgressBar" name="progressBar"/>
</item>
<item row="3" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="uploadButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<pointsize>11</pointsize>
</font>
</property>
<property name="styleSheet">
<string notr="true">padding:10px</string>
</property>
<property name="text">
<string>Upload</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="expertModeBox">
<property name="text">
<string>Expert mode</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="expertForm" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
<widget class="QTabWidget" name="tabWidget">
<property name="layoutDirection">
<enum>Qt::LeftToRight</enum>
</property>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Baudrate:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="comboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QTextBrowser" name="textBrowser">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>1</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
<property name="tabPosition">
<enum>QTabWidget::South</enum>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="programmingTab">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<attribute name="title">
<string>Flashing</string>
</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">
<property name="text">
<string>Firmware version:</string>
</property>
</widget>
</item>
<item row="5" column="0" colspan="2">
<widget class="QProgressBar" name="progressBar"/>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="versionBox">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
<item row="6" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="uploadButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Upload</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="expertModeBox">
<property name="text">
<string>Expert mode</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="boardBox">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</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">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Baudrate:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="comboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QTextBrowser" name="textBrowser">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>1</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</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>
@@ -167,8 +341,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>503</width>
<height>29</height>
<width>493</width>
<height>20</height>
</rect>
</property>
</widget>

View File

@@ -4,94 +4,149 @@
<context>
<name>MainWindow</name>
<message>
<location filename="../luftdaten-tool.py" line="138"/>
<location filename="../luftdaten-tool.py" line="132"/>
<source>No boards found</source>
<translation>Nie znaleziono płytki</translation>
</message>
<message>
<location filename="../luftdaten-tool.py" line="143"/>
<location filename="../luftdaten-tool.py" line="141"/>
<source>Others...</source>
<translation>Inne...</translation>
</message>
<message>
<location filename="../luftdaten-tool.py" line="172"/>
<location filename="../luftdaten-tool.py" line="170"/>
<source>No device selected.</source>
<translation>Nie wybrano urządzenia.</translation>
</message>
<message>
<location filename="../luftdaten-tool.py" line="176"/>
<location filename="../luftdaten-tool.py" line="174"/>
<source>No version selected.</source>
<translation>Nie wybrano wersji.</translation>
</message>
<message>
<location filename="../gui/mainwindow.py" line="131"/>
<source>Luftdaten.info Flashing Tool</source>
<translation></translation>
</message>
<message>
<location filename="../gui/mainwindow.py" line="132"/>
<location filename="../gui/mainwindow.py" line="226"/>
<source>Board:</source>
<translation>Płytka:</translation>
</message>
<message>
<location filename="../gui/mainwindow.py" line="133"/>
<location filename="../gui/mainwindow.py" line="223"/>
<source>Firmware version:</source>
<translation>Wersja oprogramowania:</translation>
</message>
<message>
<location filename="../gui/mainwindow.py" line="134"/>
<source>master</source>
<translation>master</translation>
</message>
<message>
<location filename="../gui/mainwindow.py" line="135"/>
<location filename="../gui/mainwindow.py" line="224"/>
<source>Upload</source>
<translation>Wgraj</translation>
</message>
<message>
<location filename="../gui/mainwindow.py" line="136"/>
<location filename="../gui/mainwindow.py" line="225"/>
<source>Expert mode</source>
<translation>Tryb eksperta</translation>
</message>
<message>
<location filename="../gui/mainwindow.py" line="137"/>
<location filename="../gui/mainwindow.py" line="227"/>
<source>Baudrate:</source>
<translation>Prędkość portu:</translation>
</message>
<message>
<location filename="../luftdaten-tool.py" line="191"/>
<location filename="../luftdaten-tool.py" line="189"/>
<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"/>
<location filename="../luftdaten-tool.py" line="194"/>
<source>Work in progess...</source>
<translation>Praca w toku...</translation>
</message>
<message>
<location filename="../luftdaten-tool.py" line="224"/>
<location filename="../luftdaten-tool.py" line="221"/>
<source>Downloading...</source>
<translation>Pobieranie...</translation>
</message>
<message>
<location filename="../luftdaten-tool.py" line="235"/>
<location filename="../luftdaten-tool.py" line="231"/>
<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>
<location filename="../luftdaten-tool.py" line="236"/>
<source>Connected. Chip type: {chip_type}</source>
<translation>Połączono. Typ układu: {chip_type}</translation>
</message>
<message>
<location filename="../luftdaten-tool.py" line="258"/>
<source>Writing at 0x%08x...</source>
<translation>Zapisywanie pod adresem 0x%08x...</translation>
<location filename="../luftdaten-tool.py" line="254"/>
<source>Writing at 0x{address:08x}...</source>
<translation>Zapisywanie pod adresem 0x{address:08x}...</translation>
</message>
<message>
<location filename="../luftdaten-tool.py" line="268"/>
<source>Finished in %.2f seconds</source>
<translation>Zrobione w %.2f sekundy</translation>
<location filename="../luftdaten-tool.py" line="265"/>
<source>Finished in {time:.2f} seconds. Sensor ID: {sensor_id}</source>
<translation>Zakończono w {time:.2f} sekundy. ID sensora: {sensor_id}</translation>
</message>
<message>
<location filename="../gui/mainwindow.py" line="236"/>
<source>Luftdaten.info Flashing Tool</source>
<translation>Luftdaten.info Flashing Tool</translation>
</message>
<message>
<location filename="../luftdaten-tool.py" line="52"/>
<source>Loading firmware list...</source>
<translation>Ładowanie wersji oprogramowania...</translation>
</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>
</TS>

View File

@@ -1,76 +1,39 @@
# -* encoding: utf-8 *-
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
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
# Firmware update repository
UPDATE_REPOSITORY = 'https://www.madavi.de/sensor/update/data/'
from luftdatentool.consts import UPDATE_REPOSITORY, ALLOWED_PROTO, \
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
# 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 here causes windows builds to just die. ¯\_(ツ)_/¯
logging.exception('Unhandled exception')
if getattr(sys, 'frozen', False):
RESOURCES_PATH = sys._MEIPASS
else:
RESOURCES_PATH = os.path.dirname(os.path.realpath(__file__))
class MainWindow(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
signal = QtCore.Signal([str, int])
uploadProgress = QtCore.Signal([str, int])
errorSignal = QtCore.Signal([str])
uploadThread = None
zeroconf_discovery = None
boards_detected = False
def __init__(self, parent=None, app=None):
super(MainWindow, self).__init__(parent)
@@ -78,26 +41,46 @@ class MainWindow(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
# FIXME: dirty hack to solve relative paths in *.ui
oldcwd = os.getcwd()
os.chdir('assets')
os.chdir(os.path.join(RESOURCES_PATH, 'assets'))
self.setupUi(self)
os.chdir(oldcwd)
self.app = app
self.translator = QtCore.QTranslator()
self.translator = QtCore.QTranslator()
self.i18n_init(QtCore.QLocale.system())
# TODO: extract this to separate thread
self.populate_versions()
self.populate_boards(serial.tools.list_ports.comports())
self.statusbar.showMessage(self.tr("Loading firmware list..."))
self.firmware_list = FirmwareListThread()
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.discovery_start()
self.globalMessage.hide()
# Hide WIP GUI parts...
self.on_expertModeBox_clicked()
self.expertModeBox.hide()
self.tabWidget.removeTab(self.tabWidget.indexOf(self.serialTab))
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()
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):
self.statusbar.showMessage(status)
self.progressBar.setValue(progress)
@@ -105,19 +88,42 @@ class MainWindow(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
def on_work_error(self, 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):
"""Initializes i18n to specified QLocale"""
self.app.removeTranslator(self.translator)
lang = QtCore.QLocale.languageToString(locale.language())
self.translator.load(os.path.join('i18n', lang + '.qm'))
self.translator.load(os.path.join(
RESOURCES_PATH, 'i18n', lang + '.qm'))
self.app.installTranslator(self.translator)
self.retranslateUi(self)
def populate_versions(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):
"""Loads available firmware versions into versionbox widget"""
for fname in indexof(UPDATE_REPOSITORY):
for fname in files:
if not fname.endswith('.bin'):
continue
@@ -125,10 +131,14 @@ class MainWindow(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
item.setData(UPDATE_REPOSITORY + fname, ROLE_DEVICE)
self.versionBox.model().appendRow(item)
self.statusbar.clearMessage()
def populate_boards(self, ports):
"""Populates board selection combobox from list of pyserial
ListPortInfo objects"""
self.boardBox.clear()
prefered, others = self.group_ports(ports)
for b in prefered:
@@ -142,6 +152,17 @@ class MainWindow(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
sep.setEnabled(False)
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:
sep = QtGui.QStandardItem(self.tr('Others...'))
sep.setEnabled(False)
@@ -195,13 +216,12 @@ class MainWindow(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
"Invalid version / file does not exist"))
return
if self.uploadThread and self.uploadThread.isRunning():
if self.flash_board.running():
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()
self.flash_board(self.uploadProgress, device, binary_uri,
error=self.errorSignal)
def cache_download(self, progress, binary_uri):
"""Downloads and caches file with status reports via Qt Signals"""
@@ -229,19 +249,18 @@ class MainWindow(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
return cache_fname
@QuickThread.wrap
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)
progress.emit(self.tr('Connected. Chip type: {chip_type}').format(
chip_type=esp.get_chip_description()), 0)
esp = esp.run_stub()
esp.change_baud(baudrate)
@@ -258,7 +277,8 @@ class MainWindow(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
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,),
progress.emit(self.tr('Writing at 0x{address:08x}...').format(
address=current_addr),
100 * (seq + 1) // blocks)
block = image[0:esp.FLASH_WRITE_SIZE]
@@ -268,13 +288,43 @@ class MainWindow(QtWidgets.QMainWindow, mainwindow.Ui_MainWindow):
written += len(block)
t = time.time() - t
progress.emit(self.tr('Finished in %.2f seconds') % (t,), 100)
progress.emit(self.tr(
'Finished in {time:.2f} seconds. Sensor ID: {sensor_id}').format(
time=t, sensor_id=esp.chip_id()), 100)
@QtCore.Slot()
def on_expertModeBox_clicked(self):
self.expertForm.setVisible(self.expertModeBox.checkState())
self.centralwidget.setFixedHeight(self.centralwidget.sizeHint().height())
self.setFixedHeight(self.sizeHint().height())
#self.centralwidget.setFixedHeight(
# self.centralwidget.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__":

46
luftdaten-tool.spec Normal file
View File

@@ -0,0 +1,46 @@
# -*- mode: python -*-
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'],
pathex=['.'],
binaries=[],
datas=[('assets/', './assets'), ('i18n/*.qm', './i18n')],
hiddenimports=['PyQt5.sip'],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
name='luftdaten-tool',
debug=False,
strip=False,
upx=True,
runtime_tmpdir=None,
console=False,
icon='assets/logo.ico')
# This is used on MacOS only
app = BUNDLE(exe,
name='Luftdaten.info Flashing Tool.app',
icon='assets/logo.icns',
bundle_identifier=None)

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

@@ -1,11 +1,18 @@
altgraph==0.16.1
certifi==2018.8.24
chardet==3.0.4
ecdsa==0.13
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
PyQt5==5.11.2
PyQt5-sip==4.19.12
pyserial==3.4
requests==2.19.1
urllib3==1.23
zeroconf==0.20.0