Skip to content

Commit 9b68e43

Browse files
authored
Merge pull request #123 from sparkfun/Uploader_GUI
Add Uploader GUI
2 parents aa4e55c + daae599 commit 9b68e43

File tree

8 files changed

+346
-0
lines changed

8 files changed

+346
-0
lines changed

Uploader_GUI/RTK.ico

11.3 KB
Binary file not shown.

Uploader_GUI/RTK.png

11.1 KB
Loading
Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
"""
2+
This is a simple Python3 PyQt5 firmware upload GUI for the SparkFun RTK products - based on ESP32 esptool.exe
3+
4+
Please make sure you are using Python3. You will see a bunch of errors with Python2.
5+
To install PyQt5:
6+
pip install PyQt5
7+
or
8+
pip3 install PyQt5
9+
or
10+
sudo apt-get install python3-pyqt5
11+
You may also need:
12+
sudo apt-get install python3-pyqt5.qtserialport
13+
14+
Pyinstaller:
15+
Windows:
16+
pyinstaller --onefile --clean --noconsole --distpath=./Windows_exe --icon=RTK.ico --add-binary="esptool.exe;." --add-binary="RTK_Surveyor.ino.partitions.bin;." --add-binary="RTK_Surveyor.ino.bootloader.bin;." --add-binary="boot_app0.bin;." --add-binary="RTK.png;." RTK_Firmware_Uploader_GUI.py
17+
18+
Pyinstaller needs:
19+
RTK_Firmware_Uploader_GUI.py (this file!)
20+
RTK.ico (icon file for the .exe)
21+
RTK.png (icon for the GUI widget)
22+
esptool.exe
23+
RTK_Surveyor.ino.partitions.bin
24+
RTK_Surveyor.ino.bootloader.bin
25+
boot_app0.bin
26+
27+
28+
MIT license
29+
30+
Please see the LICENSE.md for more details
31+
32+
"""
33+
34+
from typing import Iterator, Tuple
35+
36+
from PyQt5.QtCore import QSettings, QProcess, QTimer, Qt, QIODevice, pyqtSlot
37+
from PyQt5.QtWidgets import QWidget, QLabel, QComboBox, QGridLayout, \
38+
QPushButton, QApplication, QLineEdit, QFileDialog, QPlainTextEdit, \
39+
QAction, QActionGroup, QMenu, QMenuBar, QMainWindow, QMessageBox
40+
from PyQt5.QtGui import QCloseEvent, QTextCursor, QIcon, QFont
41+
from PyQt5.QtSerialPort import QSerialPortInfo, QSerialPortInfo
42+
import sys
43+
import os
44+
45+
# Setting constants
46+
SETTING_PORT_NAME = 'port_name'
47+
SETTING_FILE_LOCATION = 'message'
48+
SETTING_BAUD_RATE = '921600' # Default to 921600 for upload
49+
50+
guiVersion = 'v1.0'
51+
52+
def gen_serial_ports() -> Iterator[Tuple[str, str, str]]:
53+
"""Return all available serial ports."""
54+
ports = QSerialPortInfo.availablePorts()
55+
return ((p.description(), p.portName(), p.systemLocation()) for p in ports)
56+
57+
#https://stackoverflow.com/a/50914550
58+
def resource_path(relative_path):
59+
""" Get absolute path to resource, works for dev and for PyInstaller """
60+
base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
61+
return os.path.join(base_path, relative_path)
62+
63+
# noinspection PyArgumentList
64+
65+
class MainWidget(QWidget):
66+
"""Main Widget."""
67+
68+
def __init__(self, parent: QWidget = None) -> None:
69+
super().__init__(parent)
70+
71+
self.p = None # This will be the esptool QProcess
72+
73+
# File location line edit
74+
self.msg_label = QLabel(self.tr('Firmware File:'))
75+
self.fileLocation_lineedit = QLineEdit()
76+
self.msg_label.setBuddy(self.fileLocation_lineedit)
77+
self.fileLocation_lineedit.setEnabled(False)
78+
self.fileLocation_lineedit.returnPressed.connect(
79+
self.on_browse_btn_pressed)
80+
81+
# Browse for new file button
82+
self.browse_btn = QPushButton(self.tr('Browse'))
83+
self.browse_btn.setEnabled(True)
84+
self.browse_btn.pressed.connect(self.on_browse_btn_pressed)
85+
86+
# Port Combobox
87+
self.port_label = QLabel(self.tr('COM Port:'))
88+
self.port_combobox = QComboBox()
89+
self.port_label.setBuddy(self.port_combobox)
90+
self.update_com_ports()
91+
92+
# Refresh Button
93+
self.refresh_btn = QPushButton(self.tr('Refresh'))
94+
self.refresh_btn.clicked.connect(self.on_refresh_btn_pressed)
95+
96+
# Baudrate Combobox
97+
self.baud_label = QLabel(self.tr('Baud Rate:'))
98+
self.baud_combobox = QComboBox()
99+
self.baud_label.setBuddy(self.baud_combobox)
100+
self.update_baud_rates()
101+
102+
# Upload Button
103+
myFont=QFont()
104+
myFont.setBold(True)
105+
self.upload_btn = QPushButton(self.tr(' Upload Firmware '))
106+
self.upload_btn.setFont(myFont)
107+
self.upload_btn.clicked.connect(self.on_upload_btn_pressed)
108+
109+
# Messages Bar
110+
self.messages_label = QLabel(self.tr('Status / Warnings:'))
111+
112+
# Messages Window
113+
self.messages = QPlainTextEdit()
114+
115+
# Arrange Layout
116+
layout = QGridLayout()
117+
118+
layout.addWidget(self.msg_label, 1, 0)
119+
layout.addWidget(self.fileLocation_lineedit, 1, 1)
120+
layout.addWidget(self.browse_btn, 1, 2)
121+
122+
layout.addWidget(self.port_label, 2, 0)
123+
layout.addWidget(self.port_combobox, 2, 1)
124+
layout.addWidget(self.refresh_btn, 2, 2)
125+
126+
layout.addWidget(self.baud_label, 3, 0)
127+
layout.addWidget(self.baud_combobox, 3, 1)
128+
layout.addWidget(self.upload_btn, 3, 2)
129+
130+
layout.addWidget(self.messages_label, 4, 0)
131+
layout.addWidget(self.messages, 5, 0, 5, 3)
132+
133+
self.setLayout(layout)
134+
135+
#self._clean_settings() # This will delete all existing settings! Use with caution!
136+
137+
self._load_settings()
138+
139+
# Make the text edit window read-only
140+
self.messages.setReadOnly(True)
141+
self.messages.clear() # Clear the message window
142+
143+
def addMessage(self, msg: str) -> None:
144+
"""Add msg to the messages window, ensuring that it is visible"""
145+
self.messages.moveCursor(QTextCursor.End)
146+
self.messages.ensureCursorVisible()
147+
self.messages.appendPlainText(msg)
148+
self.messages.ensureCursorVisible()
149+
self.repaint() # Update/refresh the message window
150+
151+
def insertMessageText(self, msg: str) -> None:
152+
"""Add msg to the messages window, ensuring that it is visible"""
153+
self.messages.moveCursor(QTextCursor.End)
154+
self.messages.ensureCursorVisible()
155+
self.messages.insertPlainText(msg)
156+
self.messages.ensureCursorVisible()
157+
self.repaint() # Update/refresh the message window
158+
159+
def _load_settings(self) -> None:
160+
"""Load settings on startup."""
161+
self.settings = QSettings()
162+
163+
port_name = self.settings.value(SETTING_PORT_NAME)
164+
if port_name is not None:
165+
index = self.port_combobox.findData(port_name)
166+
if index > -1:
167+
self.port_combobox.setCurrentIndex(index)
168+
169+
lastFile = self.settings.value(SETTING_FILE_LOCATION)
170+
if lastFile is not None:
171+
self.fileLocation_lineedit.setText(lastFile)
172+
173+
baud = self.settings.value(SETTING_BAUD_RATE)
174+
if baud is not None:
175+
index = self.baud_combobox.findData(baud)
176+
if index > -1:
177+
self.baud_combobox.setCurrentIndex(index)
178+
179+
def _save_settings(self) -> None:
180+
"""Save settings on shutdown."""
181+
self.settings = QSettings()
182+
self.settings.setValue(SETTING_PORT_NAME, self.port)
183+
self.settings.setValue(SETTING_FILE_LOCATION, self.fileLocation_lineedit.text())
184+
self.settings.setValue(SETTING_BAUD_RATE, self.baudRate)
185+
186+
def _clean_settings(self) -> None:
187+
"""Clean (remove) all existing settings."""
188+
self.settings = QSettings()
189+
self.settings.clear()
190+
191+
def show_error_message(self, msg: str) -> None:
192+
"""Show a Message Box with the error message."""
193+
QMessageBox.critical(self, QApplication.applicationName(), str(msg))
194+
195+
def update_com_ports(self) -> None:
196+
"""Update COM Port list in GUI."""
197+
previousPort = self.port # Record the previous port before we clear the combobox
198+
199+
self.port_combobox.clear()
200+
201+
index = 0
202+
indexOfPrevious = -1
203+
for desc, name, sys in gen_serial_ports():
204+
longname = desc + " (" + name + ")"
205+
self.port_combobox.addItem(longname, sys)
206+
if(sys == previousPort): # Previous port still exists so record it
207+
indexOfPrevious = index
208+
index = index + 1
209+
210+
if indexOfPrevious > -1: # Restore the previous port if it still exists
211+
self.port_combobox.setCurrentIndex(indexOfPrevious)
212+
213+
def update_baud_rates(self) -> None:
214+
"""Update baud rate list in GUI."""
215+
# Highest speed first so code defaults to that
216+
# if settings.value(SETTING_BAUD_RATE) is None
217+
self.baud_combobox.clear()
218+
self.baud_combobox.addItem("921600", 921600)
219+
self.baud_combobox.addItem("460800", 460800)
220+
self.baud_combobox.addItem("115200", 115200)
221+
222+
@property
223+
def port(self) -> str:
224+
"""Return the current serial port."""
225+
return str(self.port_combobox.currentData())
226+
227+
@property
228+
def baudRate(self) -> str:
229+
"""Return the current baud rate."""
230+
return str(self.baud_combobox.currentData())
231+
232+
@property
233+
def theFileName(self) -> str:
234+
"""Return the file name."""
235+
return self.fileLocation_lineedit.text()
236+
237+
def closeEvent(self, event: QCloseEvent) -> None:
238+
"""Handle Close event of the Widget."""
239+
try:
240+
self._save_settings()
241+
except:
242+
pass
243+
244+
event.accept()
245+
246+
def on_refresh_btn_pressed(self) -> None:
247+
self.update_com_ports()
248+
self.addMessage("Ports Refreshed\n")
249+
250+
def on_browse_btn_pressed(self) -> None:
251+
"""Open dialog to select bin file."""
252+
options = QFileDialog.Options()
253+
fileName, _ = QFileDialog.getOpenFileName(
254+
None,
255+
"Select Firmware to Upload",
256+
"",
257+
"Firmware Files (*.bin);;All Files (*)",
258+
options=options)
259+
if fileName:
260+
self.fileLocation_lineedit.setText(fileName)
261+
262+
def handle_stderr(self) -> None:
263+
data = self.p.readAllStandardError()
264+
stderr = bytes(data).decode("utf8")
265+
self.insertMessageText(stderr)
266+
267+
def handle_stdout(self) -> None:
268+
data = self.p.readAllStandardOutput()
269+
stdout = bytes(data).decode("utf8")
270+
self.insertMessageText(stdout)
271+
272+
def handle_state(self, state) -> None:
273+
states = {
274+
QProcess.NotRunning: 'Not running\n',
275+
QProcess.Starting: 'Starting\n',
276+
QProcess.Running: 'Running\n',
277+
}
278+
state_name = states[state]
279+
self.addMessage(f"Upload state changed: {state_name}")
280+
281+
def process_finished(self) -> None:
282+
self.addMessage("Upload finished\n")
283+
self.p = None
284+
285+
def on_upload_btn_pressed(self) -> None:
286+
"""Upload the firmware"""
287+
portAvailable = False
288+
for desc, name, sys in gen_serial_ports():
289+
if (sys == self.port):
290+
portAvailable = True
291+
if (portAvailable == False):
292+
self.addMessage("Port No Longer Available")
293+
return
294+
295+
fileExists = False
296+
try:
297+
f = open(self.fileLocation_lineedit.text())
298+
fileExists = True
299+
except IOError:
300+
fileExists = False
301+
finally:
302+
if (fileExists == False):
303+
self.addMessage("File Not Found")
304+
return
305+
f.close()
306+
307+
if self.p is None:
308+
self.addMessage("Uploading firmware\n")
309+
310+
self.p = QProcess()
311+
self.p.readyReadStandardOutput.connect(self.handle_stdout)
312+
self.p.readyReadStandardError.connect(self.handle_stderr)
313+
self.p.stateChanged.connect(self.handle_state)
314+
self.p.finished.connect(self.process_finished)
315+
316+
command = []
317+
command.append("--chip")
318+
command.append("esp32")
319+
command.append("--port")
320+
command.append(self.port)
321+
command.append("--baud")
322+
command.append(self.baudRate)
323+
command.extend(["--before","default_reset","--after","hard_reset","write_flash","-z","--flash_mode","dio","--flash_freq","80m","--flash_size","detect"])
324+
command.extend(["0x1000",resource_path("RTK_Surveyor.ino.bootloader.bin")])
325+
command.extend(["0x8000",resource_path("RTK_Surveyor.ino.partitions.bin")])
326+
command.extend(["0xe000",resource_path("boot_app0.bin")])
327+
command.append("0x10000")
328+
command.append(self.theFileName)
329+
330+
self.addMessage("Command: esptool.py %s\n" % " ".join(command))
331+
332+
self.p.start(resource_path("esptool.exe"), command)
333+
334+
else:
335+
self.addMessage("\nUploader is already running!\n")
336+
337+
338+
if __name__ == '__main__':
339+
from sys import exit as sysExit
340+
app = QApplication([])
341+
app.setOrganizationName('SparkFun')
342+
app.setApplicationName('SparkFun RTK Firmware Uploader ' + guiVersion)
343+
app.setWindowIcon(QIcon(resource_path("RTK.png")))
344+
w = MainWidget()
345+
w.show()
346+
sys.exit(app.exec_())
17.6 KB
Binary file not shown.
Binary file not shown.
Binary file not shown.

Uploader_GUI/boot_app0.bin

8 KB
Binary file not shown.

Uploader_GUI/esptool.exe

7.28 MB
Binary file not shown.

0 commit comments

Comments
 (0)