From c40c011fe15969b8215dd6b3f4479c51c4b1883e Mon Sep 17 00:00:00 2001 From: Peter Date: Sun, 11 Apr 2021 17:36:49 +1000 Subject: [PATCH 01/47] No longer have dev branch --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7714653..fde3e93 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # PinetimeFlasher -[![Windows PyInstaller Builds](https://github.com/pfeerick/PinetimeFlasher/actions/workflows/pyinstaller-windows.yml/badge.svg?branch=dev)](https://github.com/pfeerick/PinetimeFlasher/actions/workflows/pyinstaller-windows.yml) +[![Windows PyInstaller Builds](https://github.com/pfeerick/PinetimeFlasher/actions/workflows/pyinstaller-windows.yml/badge.svg)](https://github.com/pfeerick/PinetimeFlasher/actions/workflows/pyinstaller-windows.yml) ![PinetimeFlasher](/PinetimeFlasher.png "PinetimeFlasher") From 5b4adca2ec2341d35ccbf57d963b27fe317a7a10 Mon Sep 17 00:00:00 2001 From: Peter Feerick Date: Sun, 11 Apr 2021 18:46:49 +1000 Subject: [PATCH 02/47] Add basic template for release-drafter action --- .github/release-drafter.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .github/release-drafter.yml diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..f74430d --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,4 @@ +template: | + ## What's Changed + + $CHANGES \ No newline at end of file From cc97231857e811c7d2bd7a2ccc0c36a3481c9b6f Mon Sep 17 00:00:00 2001 From: Peter Feerick Date: Sun, 11 Apr 2021 19:24:25 +1000 Subject: [PATCH 03/47] Attempt release note drafter AIO action --- .github/workflows/pyinstaller-windows.yml | 27 +++++++++++++++-------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/.github/workflows/pyinstaller-windows.yml b/.github/workflows/pyinstaller-windows.yml index 7f9b60a..a1f4d92 100644 --- a/.github/workflows/pyinstaller-windows.yml +++ b/.github/workflows/pyinstaller-windows.yml @@ -1,4 +1,4 @@ -name: Windows PyInstaller Builds +name: Windows PyInstaller Builds and Release Notes on: push: @@ -6,18 +6,17 @@ on: - main paths-ignore: - '**.md' - pull_request: - branches: - - main - paths-ignore: - - '**.md' jobs: build: - runs-on: ubuntu-latest steps: + - id: release_drafter + uses: release-drafter/release-drafter@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - uses: actions/checkout@v2 - name: Package Application @@ -25,7 +24,17 @@ jobs: with: path: . - - uses: actions/upload-artifact@v2 + - name: Upload binaries as artifact + uses: actions/upload-artifact@v2 with: name: PinetimeFlasher-for-Windows - path: ./dist/windows \ No newline at end of file + path: ./dist/windows + + - name: Upload binaries to release + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: ./dist/windows/PinetimeFlasher.exe + asset_name: PinetimeFlasher.exe + tag: ${{ steps.release_drafter.outputs.tag_name }} + overwrite: true From e0f16b12a4116b190e583d901872fab0f051a8e7 Mon Sep 17 00:00:00 2001 From: Peter Feerick Date: Sun, 11 Apr 2021 19:49:33 +1000 Subject: [PATCH 04/47] Update readme with release link from my repo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fde3e93..98b6a13 100644 --- a/README.md +++ b/README.md @@ -18,4 +18,4 @@ pip install pyinstaller pyinstaller --onefile --icon PinetimeFlasher.ico --add-data PinetimeFlasher.ico;. PinetimeFlasher.pyw ``` -Note: Pre-made executable available in the [releases](https://github.com/ZephyrLabs/PinetimeFlasher/releases) when a new version is published, as well as [automatic builds by Github Actions](https://github.com/pfeerick/PinetimeFlasher/actions/workflows/pyinstaller-windows.yml). +Note: Pre-made executable available in the [releases](https://github.com/pfeerick/PinetimeFlasher/releases) when a new version is published, as well as [automatic builds by Github Actions](https://github.com/pfeerick/PinetimeFlasher/actions/workflows/pyinstaller-windows.yml). From 3e73bd276a2616b72256081eaf5bd54800f61406 Mon Sep 17 00:00:00 2001 From: Patsy Date: Mon, 12 Apr 2021 08:33:51 +0200 Subject: [PATCH 05/47] Use pathlib to find "Downloads" directory. --- PinetimeFlasher.pyw | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index 5ac456c..e352621 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -3,7 +3,7 @@ import sys import os import shutil -import subprocess +from pathlib import Path from PyQt5.QtCore import * from PyQt5.QtWidgets import * from PyQt5.QtGui import * @@ -26,20 +26,6 @@ def progress_parser(output): return None -# Source: https://stackoverflow.com/a/48706260/4914192 -def get_download_path(): - """Returns the default downloads path for linux or windows""" - if os.name == 'nt': - import winreg - sub_key = r'SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders' - downloads_guid = '{374DE290-123F-4565-9164-39C4925E467B}' - with winreg.OpenKey(winreg.HKEY_CURRENT_USER, sub_key) as key: - location = winreg.QueryValueEx(key, downloads_guid)[0] - return location - else: - return os.path.join(os.path.expanduser('~'), 'downloads') - - # Main Program Class and UI class ptflasher(QMainWindow): def __init__(self): @@ -152,10 +138,8 @@ class ptflasher(QMainWindow): def filesearch(self): global progress, filedir - downloadsFolder = get_download_path() - datafile = self.filedialog.getOpenFileName(caption="Select firmware file to flash...", - directory=downloadsFolder, + directory=str(Path.home() / "Downloads"), filter="PineTime Firmware (*.bin *.hex)") if datafile[0] != "": From 5f76add59c3788218cf30c85caeee6a7dfc77e5e Mon Sep 17 00:00:00 2001 From: Patsy Date: Mon, 12 Apr 2021 08:59:53 +0200 Subject: [PATCH 06/47] Crash fix: catch exception when we cannot open the config file for writing. Good practice: open(...) throws OSError if a problem occurs, so catch only this exception type. --- PinetimeFlasher.pyw | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index e352621..251807c 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -85,7 +85,7 @@ class ptflasher(QMainWindow): data = pickle.load(f) default_addr = data[0] default_iface = data[1] - except: + except OSError: default_addr = "0x00008000" default_iface = "stlink.cfg" @@ -197,8 +197,7 @@ class ConfDialog(QDialog): default_iface = data[1] self.addrbox.setText(default_addr) self.ifacebox.setText(default_iface) - - except: + except OSError: self.addrbox.setText(default_addr) self.ifacebox.setText(default_iface) @@ -219,10 +218,12 @@ class ConfDialog(QDialog): iface = 'stlink.cfg' if int(addr, 0) <= 479232 and int(addr, 0) >= 0: - with open('conf.dat', 'wb+') as f: - pickle.dump((addr, iface), f) - self.status.setText('Configuration Saved.') - + try: + with open('conf.dat', 'wb+') as f: + pickle.dump((addr, iface), f) + self.status.setText('Configuration Saved.') + except OSError: + self.status.setText('Unable to write configuration file!') else: self.status.setText('Flash address is out of range!') From 0b4053c1d4cf92f4d92c26a23df246fd79775f25 Mon Sep 17 00:00:00 2001 From: Patsy Date: Mon, 12 Apr 2021 09:00:52 +0200 Subject: [PATCH 07/47] Remove redundant checks for empty iface/addr values. --- PinetimeFlasher.pyw | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index 251807c..9f82ca5 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -211,11 +211,10 @@ class ConfDialog(QDialog): addr = self.addrbox.toPlainText() iface = self.ifacebox.toPlainText() - if addr == '' or iface == '': - if addr == '': - addr = '0x00008000' - if iface == '': - iface = 'stlink.cfg' + if addr == '': + addr = '0x00008000' + if iface == '': + iface = 'stlink.cfg' if int(addr, 0) <= 479232 and int(addr, 0) >= 0: try: From 6140723e08e19ac698bbafe62cf58ea64c1a1023 Mon Sep 17 00:00:00 2001 From: Patsy Date: Mon, 12 Apr 2021 09:06:17 +0200 Subject: [PATCH 08/47] Use "variable = value or default" idiom to remove an explicit conditional. --- PinetimeFlasher.pyw | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index 9f82ca5..541b525 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -208,13 +208,8 @@ class ConfDialog(QDialog): def saveconf(self, s): global addrbox, ifacebox, status - addr = self.addrbox.toPlainText() - iface = self.ifacebox.toPlainText() - - if addr == '': - addr = '0x00008000' - if iface == '': - iface = 'stlink.cfg' + addr = self.addrbox.toPlainText() or '0x00008000' + iface = self.ifacebox.toPlainText() or 'stlink.cfg' if int(addr, 0) <= 479232 and int(addr, 0) >= 0: try: From 886db0e1f1078e2d24c081ca1386043314aa7e83 Mon Sep 17 00:00:00 2001 From: Patsy Date: Mon, 12 Apr 2021 09:15:45 +0200 Subject: [PATCH 09/47] Validate address using hexadecimal representation, as this is how the address is stored. --- PinetimeFlasher.pyw | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index 541b525..c0778c5 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -211,7 +211,7 @@ class ConfDialog(QDialog): addr = self.addrbox.toPlainText() or '0x00008000' iface = self.ifacebox.toPlainText() or 'stlink.cfg' - if int(addr, 0) <= 479232 and int(addr, 0) >= 0: + if int(addr, 0) <= 0x00075000 and int(addr, 0) >= 0: try: with open('conf.dat', 'wb+') as f: pickle.dump((addr, iface), f) From bc05fea4e292bf5babde37622b3015d3294323f9 Mon Sep 17 00:00:00 2001 From: Patsy Date: Mon, 12 Apr 2021 09:24:27 +0200 Subject: [PATCH 10/47] Use idiomatic comparisons with None. It's not necessary to say "if x is not None:" or "if x is None:" - "if x:" and "if not x:" are just equivalent but more idiomatic. In startflash, return early if the process is already running - allows the scope of the rest of the function to be taken outside the conditinal. --- PinetimeFlasher.pyw | 70 ++++++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index c0778c5..9135830 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -22,8 +22,6 @@ def progress_parser(output): return 90 elif "** Resetting Target **" in output: return 100 - else: - return None # Main Program Class and UI @@ -73,50 +71,52 @@ class ptflasher(QMainWindow): self.setCentralWidget(w) def startflash(self): - if self.p is None: # If process not already running - global progress + if self.p: # if process is already running + return - self.progress.setValue(0) - - source = self.filedir.toPlainText() + global progress - try: - with open('conf.dat', 'rb+') as f: - data = pickle.load(f) - default_addr = data[0] - default_iface = data[1] - except OSError: - default_addr = "0x00008000" - default_iface = "stlink.cfg" + self.progress.setValue(0) - self.progress.setValue(10) + source = self.filedir.toPlainText() - if os.path.exists(source): - if shutil.which('openocd') is not None: - self.status.setText('Flashing...') - self.status.repaint() + try: + with open('conf.dat', 'rb+') as f: + data = pickle.load(f) + default_addr = data[0] + default_iface = data[1] + except OSError: + default_addr = "0x00008000" + default_iface = "stlink.cfg" - command = ('openocd -f "interface/{}" ' - '-f "target/nrf52.cfg" -c "init" ' - '-c "program {} {} verify reset exit"').format( - default_iface, source, default_addr) + self.progress.setValue(10) - self.p = QProcess() # Keep a reference while it's running - self.p.finished.connect(self.flash_finished) # Clean up - self.p.readyReadStandardError.connect(self.handle_stderr) - self.p.start(command) + if os.path.exists(source): + if shutil.which('openocd'): + self.status.setText('Flashing...') + self.status.repaint() - else: - self.progress.setValue(0) - self.status.setText("OpenOCD not found in system path!") + command = ('openocd -f "interface/{}" ' + '-f "target/nrf52.cfg" -c "init" ' + '-c "program {} {} verify reset exit"').format( + default_iface, source, default_addr) - elif source == '': - self.status.setText("Set location of file to be flashed!") - self.progress.setValue(0) + self.p = QProcess() # Keep a reference while it's running + self.p.finished.connect(self.flash_finished) # Clean up + self.p.readyReadStandardError.connect(self.handle_stderr) + self.p.start(command) else: - self.status.setText("File does not exist!") self.progress.setValue(0) + self.status.setText("OpenOCD not found in system path!") + + elif source == '': + self.status.setText("Set location of file to be flashed!") + self.progress.setValue(0) + + else: + self.status.setText("File does not exist!") + self.progress.setValue(0) def flash_finished(self, ): if (self.p.exitCode() == 0): From 302a5725af806ba68be751efaa9af5012df9290e Mon Sep 17 00:00:00 2001 From: Patsy Date: Mon, 12 Apr 2021 09:25:51 +0200 Subject: [PATCH 11/47] Python doesn't require parentheses in 'if' clauses. --- PinetimeFlasher.pyw | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index 9135830..9ab74d6 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -119,7 +119,7 @@ class ptflasher(QMainWindow): self.progress.setValue(0) def flash_finished(self, ): - if (self.p.exitCode() == 0): + if self.p.exitCode() == 0: self.status.setText('Success!') else: self.status.setText('Something probably went wrong :(') From 9e6ecb0bdfbe7871f1aaf03a7f1aab9905b28492 Mon Sep 17 00:00:00 2001 From: Patsy Date: Mon, 12 Apr 2021 09:30:34 +0200 Subject: [PATCH 12/47] Show the valid address range when a user tries to input and invalid value. --- PinetimeFlasher.pyw | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index 9ab74d6..62722dc 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -219,7 +219,7 @@ class ConfDialog(QDialog): except OSError: self.status.setText('Unable to write configuration file!') else: - self.status.setText('Flash address is out of range!') + self.status.setText('Flash address not in the valid range (0x0 - 0x00075000)') def infoButton(self, s): dlg = InfoDialog() From 8079e0a410095414b40bd422c3011707d6a9787c Mon Sep 17 00:00:00 2001 From: Peter Feerick Date: Tue, 13 Apr 2021 10:20:39 +1000 Subject: [PATCH 13/47] Consistent use of " instead ' and " --- PinetimeFlasher.pyw | 85 ++++++++++++++++++++++++--------------------- 1 file changed, 45 insertions(+), 40 deletions(-) diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index 62722dc..e9701d0 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -31,10 +31,10 @@ class ptflasher(QMainWindow): self.p = None # Default empty value. - self.setWindowTitle('PineTime Flasher') + self.setWindowTitle("PineTime Flasher") self.resize(300, 200) - self.info = QLabel('Enter The Path Of The File To Be Flashed') + self.info = QLabel("Enter The Path Of The File To Be Flashed") self.filedir = QTextEdit() filedir = self.filedir.toPlainText() @@ -44,14 +44,14 @@ class ptflasher(QMainWindow): self.progress.setMaximum(100) self.progress.setValue(0) - self.flashbtn = QPushButton('Start Flashing') - self.searchbtn = QPushButton('Search for File') - self.confbtn = QPushButton('Configure flashing options...') + self.flashbtn = QPushButton("Start Flashing") + self.searchbtn = QPushButton("Search for File") + self.confbtn = QPushButton("Configure flashing options...") self.flashbtn.clicked.connect(self.startflash) self.searchbtn.clicked.connect(self.filesearch) self.confbtn.clicked.connect(self.confButton) - self.status = QLabel('Ready.') + self.status = QLabel("Ready.") self.filedialog = QFileDialog() @@ -81,7 +81,7 @@ class ptflasher(QMainWindow): source = self.filedir.toPlainText() try: - with open('conf.dat', 'rb+') as f: + with open("conf.dat", "rb+") as f: data = pickle.load(f) default_addr = data[0] default_iface = data[1] @@ -92,14 +92,15 @@ class ptflasher(QMainWindow): self.progress.setValue(10) if os.path.exists(source): - if shutil.which('openocd'): - self.status.setText('Flashing...') + if shutil.which("openocd"): + self.status.setText("Flashing...") self.status.repaint() - command = ('openocd -f "interface/{}" ' - '-f "target/nrf52.cfg" -c "init" ' - '-c "program {} {} verify reset exit"').format( - default_iface, source, default_addr) + command = ( + 'openocd -f "interface/{}" ' + '-f "target/nrf52.cfg" -c "init" ' + '-c "program {} {} verify reset exit"' + ).format(default_iface, source, default_addr) self.p = QProcess() # Keep a reference while it's running self.p.finished.connect(self.flash_finished) # Clean up @@ -110,7 +111,7 @@ class ptflasher(QMainWindow): self.progress.setValue(0) self.status.setText("OpenOCD not found in system path!") - elif source == '': + elif source == "": self.status.setText("Set location of file to be flashed!") self.progress.setValue(0) @@ -118,11 +119,11 @@ class ptflasher(QMainWindow): self.status.setText("File does not exist!") self.progress.setValue(0) - def flash_finished(self, ): + def flash_finished(self): if self.p.exitCode() == 0: - self.status.setText('Success!') + self.status.setText("Success!") else: - self.status.setText('Something probably went wrong :(') + self.status.setText("Something probably went wrong :(") self.progress.setValue(0) self.p = None @@ -138,9 +139,11 @@ class ptflasher(QMainWindow): def filesearch(self): global progress, filedir - datafile = self.filedialog.getOpenFileName(caption="Select firmware file to flash...", - directory=str(Path.home() / "Downloads"), - filter="PineTime Firmware (*.bin *.hex)") + datafile = self.filedialog.getOpenFileName( + caption="Select firmware file to flash...", + directory=str(Path.home() / "Downloads"), + filter="PineTime Firmware (*.bin *.hex)", + ) if datafile[0] != "": self.filedir.setText(datafile[0]) @@ -159,19 +162,19 @@ class ConfDialog(QDialog): default_addr = "0x00008000" default_iface = "stlink.cfg" - self.setWindowTitle('Flash Configuration') + self.setWindowTitle("Flash Configuration") self.resize(300, 200) - self.addrinfo = QLabel('Enter the Flash Address (default 0x00008000)') - self.ifaceinfo = QLabel('Enter the Interface (default stlink)') + self.addrinfo = QLabel("Enter the Flash Address (default 0x00008000)") + self.ifaceinfo = QLabel("Enter the Interface (default stlink)") self.addrbox = QTextEdit() self.ifacebox = QTextEdit() - self.savebtn = QPushButton('Save configuration') - self.infobtn = QPushButton('More info') + self.savebtn = QPushButton("Save configuration") + self.infobtn = QPushButton("More info") - self.status = QLabel('') + self.status = QLabel("") conflayout = QVBoxLayout() confbuttonrow = QHBoxLayout() @@ -191,7 +194,7 @@ class ConfDialog(QDialog): self.setLayout(conflayout) try: - with open('conf.dat', 'rb+') as f: + with open("conf.dat", "rb+") as f: data = pickle.load(f) default_addr = data[0] default_iface = data[1] @@ -208,18 +211,20 @@ class ConfDialog(QDialog): def saveconf(self, s): global addrbox, ifacebox, status - addr = self.addrbox.toPlainText() or '0x00008000' - iface = self.ifacebox.toPlainText() or 'stlink.cfg' + addr = self.addrbox.toPlainText() or "0x00008000" + iface = self.ifacebox.toPlainText() or "stlink.cfg" if int(addr, 0) <= 0x00075000 and int(addr, 0) >= 0: try: - with open('conf.dat', 'wb+') as f: + with open("conf.dat", "wb+") as f: pickle.dump((addr, iface), f) - self.status.setText('Configuration Saved.') + self.status.setText("Configuration Saved.") except OSError: - self.status.setText('Unable to write configuration file!') + self.status.setText("Unable to write configuration file!") else: - self.status.setText('Flash address not in the valid range (0x0 - 0x00075000)') + self.status.setText( + "Flash address not in the valid range (0x0 - 0x00075000)" + ) def infoButton(self, s): dlg = InfoDialog() @@ -234,11 +239,11 @@ class InfoDialog(QDialog): default_addr = "0x00008000" default_iface = "stlink.cfg" - self.setWindowTitle('About PineTime Flasher') + self.setWindowTitle("About PineTime Flasher") self.resize(450, 200) vbox = QVBoxLayout() - text = ''' + text = """ PineTime Flasher is a simple GUI software written in Python, using the xpack-openOCD tool for flashing the PineTime with either ST-Link, J-Link etc. @@ -253,7 +258,7 @@ class InfoDialog(QDialog): For the interface, the options available are dependent on the (*.cfg) provided by the xpack-openOCD itself. For example: - stlink.cfg or jlink.cfg''' + stlink.cfg or jlink.cfg""" textView = QPlainTextEdit() textView.setPlainText(text) @@ -266,13 +271,13 @@ class InfoDialog(QDialog): # Program entrypoint -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication(sys.argv) app.setStyle("Fusion") - appDir = getattr(sys, '_MEIPASS', os.path.abspath( - os.path.dirname(__file__))) - path_to_icon = os.path.abspath(os.path.join(appDir, 'PinetimeFlasher.ico')) + appDir = getattr(sys, "_MEIPASS", + os.path.abspath(os.path.dirname(__file__))) + path_to_icon = os.path.abspath(os.path.join(appDir, "PinetimeFlasher.ico")) app_icon = QIcon(path_to_icon) app.setWindowIcon(app_icon) From e2b7e374c28542e290f576d3b74320fcda958657 Mon Sep 17 00:00:00 2001 From: Patsy Date: Tue, 13 Apr 2021 19:23:22 +0200 Subject: [PATCH 14/47] Remove unnecessary variables/global declarations. --- .idea/vcs.xml | 6 ++ .idea/workspace.xml | 172 ++++++++++++++++++++++++++++++++++++++++++++ PinetimeFlasher.pyw | 8 --- 3 files changed, 178 insertions(+), 8 deletions(-) create mode 100644 .idea/vcs.xml create mode 100644 .idea/workspace.xml diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..1c3581e --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + 1618207563887 + + + 1618210793437 + + + 1618211745258 + + + 1618212267211 + + + 1618212351907 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + file://$PROJECT_DIR$/PinetimeFlasher.pyw + 104 + + + + + \ No newline at end of file diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index e9701d0..c0a9823 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -74,8 +74,6 @@ class ptflasher(QMainWindow): if self.p: # if process is already running return - global progress - self.progress.setValue(0) source = self.filedir.toPlainText() @@ -137,8 +135,6 @@ class ptflasher(QMainWindow): self.status.setText("Verifying...") def filesearch(self): - global progress, filedir - datafile = self.filedialog.getOpenFileName( caption="Select firmware file to flash...", directory=str(Path.home() / "Downloads"), @@ -210,7 +206,6 @@ class ConfDialog(QDialog): self.setWindowModality(Qt.ApplicationModal) def saveconf(self, s): - global addrbox, ifacebox, status addr = self.addrbox.toPlainText() or "0x00008000" iface = self.ifacebox.toPlainText() or "stlink.cfg" @@ -236,9 +231,6 @@ class InfoDialog(QDialog): def __init__(self, parent=None): super().__init__(parent=parent) - default_addr = "0x00008000" - default_iface = "stlink.cfg" - self.setWindowTitle("About PineTime Flasher") self.resize(450, 200) From 9002ce9847654b9bfbc4e45f36420a169c96f61a Mon Sep 17 00:00:00 2001 From: Patsy Date: Tue, 13 Apr 2021 19:30:16 +0200 Subject: [PATCH 15/47] Reduce nesting and exit early on errors in startflash. --- PinetimeFlasher.pyw | 45 +++++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index c0a9823..60ad4ae 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -89,33 +89,34 @@ class ptflasher(QMainWindow): self.progress.setValue(10) - if os.path.exists(source): - if shutil.which("openocd"): - self.status.setText("Flashing...") - self.status.repaint() - - command = ( - 'openocd -f "interface/{}" ' - '-f "target/nrf52.cfg" -c "init" ' - '-c "program {} {} verify reset exit"' - ).format(default_iface, source, default_addr) - - self.p = QProcess() # Keep a reference while it's running - self.p.finished.connect(self.flash_finished) # Clean up - self.p.readyReadStandardError.connect(self.handle_stderr) - self.p.start(command) - - else: - self.progress.setValue(0) - self.status.setText("OpenOCD not found in system path!") - - elif source == "": + if source == "": self.status.setText("Set location of file to be flashed!") self.progress.setValue(0) + return - else: + if not os.path.exists(source): self.status.setText("File does not exist!") self.progress.setValue(0) + return + + if not shutil.which("openocd"): + self.status.setText("OpenOCD not found in system path!") + self.progress.setValue(0) + return + + self.status.setText("Flashing...") + self.status.repaint() + + command = ( + 'openocd -f "interface/{}" ' + '-f "target/nrf52.cfg" -c "init" ' + '-c "program {} {} verify reset exit"' + ).format(default_iface, source, default_addr) + + self.p = QProcess() # Keep a reference while it's running + self.p.finished.connect(self.flash_finished) # Clean up + self.p.readyReadStandardError.connect(self.handle_stderr) + self.p.start(command) def flash_finished(self): if self.p.exitCode() == 0: From 69451708087933993c5d4bd6985529bc984e425f Mon Sep 17 00:00:00 2001 From: Patsy Date: Tue, 13 Apr 2021 21:17:08 +0200 Subject: [PATCH 16/47] Use a combobox to select firmware type. The corresponding addresses are stored in the 'userdata'. It's less direct, but allows the user to see a much friendlier option. It means that if there are only two correct addresses, the possibility of a user inserting incorrect values is removed (and the need for validation is similarly removed). Related to this: reading the config file is now pulled out into a separate function that returns default values on any file read errors. --- .idea/workspace.xml | 44 +++++++++------------- PinetimeFlasher.pyw | 91 +++++++++++++++++++++++++-------------------- 2 files changed, 67 insertions(+), 68 deletions(-) diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 1c3581e..7021f62 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -2,6 +2,7 @@ + - - - - - file://$PROJECT_DIR$/PinetimeFlasher.pyw - 104 - - - - \ No newline at end of file diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index 60ad4ae..095d111 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -24,6 +24,23 @@ def progress_parser(output): return 100 +def read_config_file(status_notice): + """ + Return the (address, interface) as read from the config file. + Returns default values for both if there are any file access errors. + """ + try: + with open("conf.dat", "rb+") as f: + data = pickle.load(f) + address = data[0] + interface = data[1] + return address, interface + except OSError: + status_notice.setText("Unable to read config file - using default values!") + + return "0x00008000", "stlink.cfg" + + # Main Program Class and UI class ptflasher(QMainWindow): def __init__(self): @@ -77,15 +94,7 @@ class ptflasher(QMainWindow): self.progress.setValue(0) source = self.filedir.toPlainText() - - try: - with open("conf.dat", "rb+") as f: - data = pickle.load(f) - default_addr = data[0] - default_iface = data[1] - except OSError: - default_addr = "0x00008000" - default_iface = "stlink.cfg" + address, interface = read_config_file(self.status) self.progress.setValue(10) @@ -111,7 +120,7 @@ class ptflasher(QMainWindow): 'openocd -f "interface/{}" ' '-f "target/nrf52.cfg" -c "init" ' '-c "program {} {} verify reset exit"' - ).format(default_iface, source, default_addr) + ).format(interface, source, address) self.p = QProcess() # Keep a reference while it's running self.p.finished.connect(self.flash_finished) # Clean up @@ -156,16 +165,18 @@ class ConfDialog(QDialog): def __init__(self, parent=None): super().__init__(parent=parent) - default_addr = "0x00008000" - default_iface = "stlink.cfg" + self.firmware_types = [ + {"name": "mcuboot-app", "address": "0x00008000"}, + {"name": "bootloader", "address": "0x00000000"} + ] self.setWindowTitle("Flash Configuration") self.resize(300, 200) - self.addrinfo = QLabel("Enter the Flash Address (default 0x00008000)") + self.addrinfo = QLabel("Firmware type (used to determine address):") self.ifaceinfo = QLabel("Enter the Interface (default stlink)") - self.addrbox = QTextEdit() + self.addrbox = QComboBox() self.ifacebox = QTextEdit() self.savebtn = QPushButton("Save configuration") @@ -176,6 +187,9 @@ class ConfDialog(QDialog): conflayout = QVBoxLayout() confbuttonrow = QHBoxLayout() + for firmware in self.firmware_types: + self.addrbox.addItem(firmware["name"], firmware["address"]) + conflayout.addWidget(self.addrinfo) conflayout.addWidget(self.addrbox) conflayout.addWidget(self.ifaceinfo) @@ -190,37 +204,32 @@ class ConfDialog(QDialog): self.setLayout(conflayout) - try: - with open("conf.dat", "rb+") as f: - data = pickle.load(f) - default_addr = data[0] - default_iface = data[1] - self.addrbox.setText(default_addr) - self.ifacebox.setText(default_iface) - except OSError: - self.addrbox.setText(default_addr) - self.ifacebox.setText(default_iface) + address, interface = read_config_file(self.status) + self.addrbox.setCurrentIndex(self.get_firmware_index(address)) + self.ifacebox.setText(interface) + self.addrbox.setCurrentIndex(0) self.infobtn.clicked.connect(self.infoButton) self.savebtn.clicked.connect(self.saveconf) self.setWindowModality(Qt.ApplicationModal) + def get_firmware_index(self, address:str): + for i, firmware in enumerate(self.firmware_types): + if firmware["address"] == address: + return i + return 0 + def saveconf(self, s): - addr = self.addrbox.toPlainText() or "0x00008000" + addr = self.addrbox.currentData() iface = self.ifacebox.toPlainText() or "stlink.cfg" - if int(addr, 0) <= 0x00075000 and int(addr, 0) >= 0: - try: - with open("conf.dat", "wb+") as f: - pickle.dump((addr, iface), f) - self.status.setText("Configuration Saved.") - except OSError: - self.status.setText("Unable to write configuration file!") - else: - self.status.setText( - "Flash address not in the valid range (0x0 - 0x00075000)" - ) + try: + with open("conf.dat", "wb+") as f: + pickle.dump((addr, iface), f) + self.status.setText("Configuration Saved.") + except OSError: + self.status.setText("Unable to write configuration file!") def infoButton(self, s): dlg = InfoDialog() @@ -242,12 +251,12 @@ class InfoDialog(QDialog): either ST-Link, J-Link etc. When first using the software, it is recommended that you - setup the configuration by choosing the appropriate flashing - address and flashing interface. + setup the configuration by choosing the appropriate firmware + type and flashing interface. - The possible addresses are: - 0x00 (for the bootloader) - 0x00008000 (for mcuboot-app) + The possible firmware types are: + * bootloader + * mcuboot-app For the interface, the options available are dependent on the (*.cfg) provided by the xpack-openOCD itself. For example: From b306c7d1031a92b2e84cb8cd578d69ac0ac37530 Mon Sep 17 00:00:00 2001 From: Patsy Date: Tue, 13 Apr 2021 21:33:16 +0200 Subject: [PATCH 17/47] Use QPlainTextEdit instead of QTextEdit since we don't need formatting information. QTextEdit.toPlainText returns the contents as plain text, rather than switching it to plain text most: QPlainTextEdit must be used for that instead. --- .idea/workspace.xml | 48 ++++++++++++++++++++++++++++++--------------- PinetimeFlasher.pyw | 9 ++++----- 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 7021f62..92a4039 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -106,38 +106,38 @@ - + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + @@ -159,4 +159,20 @@ + + + + + file://$PROJECT_DIR$/PinetimeFlasher.pyw + 219 + + + file://$PROJECT_DIR$/PinetimeFlasher.pyw + 226 + + + + \ No newline at end of file diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index 095d111..a0eda61 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -53,8 +53,7 @@ class ptflasher(QMainWindow): self.info = QLabel("Enter The Path Of The File To Be Flashed") - self.filedir = QTextEdit() - filedir = self.filedir.toPlainText() + self.filedir = QPlainTextEdit() self.progress = QProgressBar() self.progress.setMinimum(0) @@ -177,7 +176,7 @@ class ConfDialog(QDialog): self.ifaceinfo = QLabel("Enter the Interface (default stlink)") self.addrbox = QComboBox() - self.ifacebox = QTextEdit() + self.ifacebox = QPlainTextEdit() self.savebtn = QPushButton("Save configuration") self.infobtn = QPushButton("More info") @@ -206,7 +205,7 @@ class ConfDialog(QDialog): address, interface = read_config_file(self.status) self.addrbox.setCurrentIndex(self.get_firmware_index(address)) - self.ifacebox.setText(interface) + self.ifacebox.setPlainText(interface) self.addrbox.setCurrentIndex(0) self.infobtn.clicked.connect(self.infoButton) @@ -214,7 +213,7 @@ class ConfDialog(QDialog): self.setWindowModality(Qt.ApplicationModal) - def get_firmware_index(self, address:str): + def get_firmware_index(self, address: str): for i, firmware in enumerate(self.firmware_types): if firmware["address"] == address: return i From d9c378b49e062baf0526262b775d630b7c7a8216 Mon Sep 17 00:00:00 2001 From: Patsy Date: Tue, 13 Apr 2021 21:34:51 +0200 Subject: [PATCH 18/47] Remove line I only inserted for testing! --- PinetimeFlasher.pyw | 1 - 1 file changed, 1 deletion(-) diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index a0eda61..c55ce2d 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -207,7 +207,6 @@ class ConfDialog(QDialog): self.addrbox.setCurrentIndex(self.get_firmware_index(address)) self.ifacebox.setPlainText(interface) - self.addrbox.setCurrentIndex(0) self.infobtn.clicked.connect(self.infoButton) self.savebtn.clicked.connect(self.saveconf) From d9904473737c20e63406142c0e93cd6c62208eb1 Mon Sep 17 00:00:00 2001 From: Patsy Date: Tue, 13 Apr 2021 21:39:40 +0200 Subject: [PATCH 19/47] Don't use quite so many capital letters in the UI. --- PinetimeFlasher.pyw | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index c55ce2d..00ba24e 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -51,7 +51,7 @@ class ptflasher(QMainWindow): self.setWindowTitle("PineTime Flasher") self.resize(300, 200) - self.info = QLabel("Enter The Path Of The File To Be Flashed") + self.info = QLabel("Enter the path of the file to be flashed") self.filedir = QPlainTextEdit() @@ -60,7 +60,7 @@ class ptflasher(QMainWindow): self.progress.setMaximum(100) self.progress.setValue(0) - self.flashbtn = QPushButton("Start Flashing") + self.flashbtn = QPushButton("Start flashing") self.searchbtn = QPushButton("Search for File") self.confbtn = QPushButton("Configure flashing options...") self.flashbtn.clicked.connect(self.startflash) From 3b7e640afd1ccdc56a0745831be8a8832221d10b Mon Sep 17 00:00:00 2001 From: Patsy Date: Tue, 13 Apr 2021 21:40:37 +0200 Subject: [PATCH 20/47] Default interface is "stlink.cfg", not "stlink". --- PinetimeFlasher.pyw | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index 00ba24e..70422d8 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -173,7 +173,7 @@ class ConfDialog(QDialog): self.resize(300, 200) self.addrinfo = QLabel("Firmware type (used to determine address):") - self.ifaceinfo = QLabel("Enter the Interface (default stlink)") + self.ifaceinfo = QLabel("Enter the interface (default: stlink.cfg)") self.addrbox = QComboBox() self.ifacebox = QPlainTextEdit() From 8081046e718717a3656572fefc9331c1ae14b191 Mon Sep 17 00:00:00 2001 From: Patsy Date: Tue, 13 Apr 2021 21:41:44 +0200 Subject: [PATCH 21/47] Use consistent ordering of firmware types. Not hugely important, but such consi8stency ensures a better UX. --- PinetimeFlasher.pyw | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index 70422d8..e9d9d54 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -253,8 +253,8 @@ class InfoDialog(QDialog): type and flashing interface. The possible firmware types are: - * bootloader * mcuboot-app + * bootloader For the interface, the options available are dependent on the (*.cfg) provided by the xpack-openOCD itself. For example: From 0919673df5cd6ae34643c47c1f326722da437b57 Mon Sep 17 00:00:00 2001 From: Patsy Date: Tue, 13 Apr 2021 21:42:51 +0200 Subject: [PATCH 22/47] Minor wording tweak in 'about' dialog. "using" -> "uses" for tense consistency. remove "either" since there are more than two interfaces. --- PinetimeFlasher.pyw | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index e9d9d54..026c6ae 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -245,8 +245,8 @@ class InfoDialog(QDialog): vbox = QVBoxLayout() text = """ PineTime Flasher is a simple GUI software written in Python, - using the xpack-openOCD tool for flashing the PineTime with - either ST-Link, J-Link etc. + that uses the xpack-openOCD tool for flashing the PineTime + with ST-Link, J-Link etc. When first using the software, it is recommended that you setup the configuration by choosing the appropriate firmware From ef99657d68c838738080201c8fc6dd72b4e91fdf Mon Sep 17 00:00:00 2001 From: Patsy Date: Tue, 13 Apr 2021 21:50:59 +0200 Subject: [PATCH 23/47] In the 'about' dialog, display text using a label instead of a read-only text edit. This allows the dialog to be smaller. With triple-quoted strings, even the indenting whitespace is included as part of the string, so they need to sit against the left-hand side of the file. It's a bit ugly, but c'est la vie :) --- PinetimeFlasher.pyw | 39 +++++++++++++++++---------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index 026c6ae..ef95569 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -240,30 +240,25 @@ class InfoDialog(QDialog): super().__init__(parent=parent) self.setWindowTitle("About PineTime Flasher") - self.resize(450, 200) + self.resize(300, 200) vbox = QVBoxLayout() - text = """ - PineTime Flasher is a simple GUI software written in Python, - that uses the xpack-openOCD tool for flashing the PineTime - with ST-Link, J-Link etc. - - When first using the software, it is recommended that you - setup the configuration by choosing the appropriate firmware - type and flashing interface. - - The possible firmware types are: - * mcuboot-app - * bootloader - - For the interface, the options available are dependent on the - (*.cfg) provided by the xpack-openOCD itself. For example: - stlink.cfg or jlink.cfg""" - - textView = QPlainTextEdit() - textView.setPlainText(text) - textView.setReadOnly(True) - + textView = QLabel( +"""PineTime Flasher is a simple GUI software written in Python, +that uses the xpack-openOCD tool for flashing the PineTime +with ST-Link, J-Link etc. + +When first using the software, it is recommended that you +setup the configuration by choosing the appropriate firmware +type and flashing interface. + +The possible firmware types are: +* mcuboot-app +* bootloader + +For the interface, the options available are dependent on the +(*.cfg) provided by the xpack-openOCD itself. For example: +stlink.cfg or jlink.cfg""") vbox.addWidget(textView) self.setLayout(vbox) From c910e4f78629d5803a70f08a4aabc8c6dddc9421 Mon Sep 17 00:00:00 2001 From: Patsy Date: Tue, 13 Apr 2021 22:06:10 +0200 Subject: [PATCH 24/47] Update the last setText call to setPlainText. --- PinetimeFlasher.pyw | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index ef95569..71f3ac8 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -151,7 +151,7 @@ class ptflasher(QMainWindow): ) if datafile[0] != "": - self.filedir.setText(datafile[0]) + self.filedir.setPlainText(datafile[0]) self.progress.setValue(0) def confButton(self, s): From e63511cd33773b7503af70af2ffb6d821f6592f3 Mon Sep 17 00:00:00 2001 From: Patsy Date: Tue, 13 Apr 2021 22:06:35 +0200 Subject: [PATCH 25/47] Add type hints for read_config_file. --- PinetimeFlasher.pyw | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index 71f3ac8..f521e29 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -24,7 +24,7 @@ def progress_parser(output): return 100 -def read_config_file(status_notice): +def read_config_file(status_notice: QLabel) -> (str, str): """ Return the (address, interface) as read from the config file. Returns default values for both if there are any file access errors. From 77f0ab0ed7d9c1ec5bd4ed320ea377f338e46788 Mon Sep 17 00:00:00 2001 From: Patsy Date: Tue, 13 Apr 2021 22:08:17 +0200 Subject: [PATCH 26/47] Reduce scope of ptflasher.filedialog. It's good practice to reduce the scope of variables as much as possible, and it turns out ptflasher.filedialog can be turned into a local variable inside ptflasher.filesearch. --- PinetimeFlasher.pyw | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index f521e29..fdcc7a4 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -69,8 +69,6 @@ class ptflasher(QMainWindow): self.status = QLabel("Ready.") - self.filedialog = QFileDialog() - layout = QVBoxLayout() layout.addWidget(self.info) @@ -144,7 +142,8 @@ class ptflasher(QMainWindow): self.status.setText("Verifying...") def filesearch(self): - datafile = self.filedialog.getOpenFileName( + filedialog = QFileDialog() + datafile = filedialog.getOpenFileName( caption="Select firmware file to flash...", directory=str(Path.home() / "Downloads"), filter="PineTime Firmware (*.bin *.hex)", From d0ce7d3ea3bfdce18ab918275657067474923f74 Mon Sep 17 00:00:00 2001 From: Patsy Date: Tue, 13 Apr 2021 22:15:41 +0200 Subject: [PATCH 27/47] Rearrange definitions in ConfDialog constructor. This makes the order of definitions consistent with their appearence in the dialog itself, and consistency is (almost) always helpful :) --- PinetimeFlasher.pyw | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index fdcc7a4..5f98059 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -172,9 +172,11 @@ class ConfDialog(QDialog): self.resize(300, 200) self.addrinfo = QLabel("Firmware type (used to determine address):") - self.ifaceinfo = QLabel("Enter the interface (default: stlink.cfg)") - self.addrbox = QComboBox() + for firmware in self.firmware_types: + self.addrbox.addItem(firmware["name"], firmware["address"]) + + self.ifaceinfo = QLabel("Enter the interface (default: stlink.cfg)") self.ifacebox = QPlainTextEdit() self.savebtn = QPushButton("Save configuration") @@ -185,9 +187,6 @@ class ConfDialog(QDialog): conflayout = QVBoxLayout() confbuttonrow = QHBoxLayout() - for firmware in self.firmware_types: - self.addrbox.addItem(firmware["name"], firmware["address"]) - conflayout.addWidget(self.addrinfo) conflayout.addWidget(self.addrbox) conflayout.addWidget(self.ifaceinfo) @@ -197,7 +196,6 @@ class ConfDialog(QDialog): confbuttonrow.addWidget(self.infobtn) conflayout.addLayout(confbuttonrow) - conflayout.addWidget(self.status) self.setLayout(conflayout) From fc68abbd2dec47ecd79e1fa2a5fb13534aab31c0 Mon Sep 17 00:00:00 2001 From: Patsy Date: Tue, 13 Apr 2021 22:18:24 +0200 Subject: [PATCH 28/47] "if " implicitly compares against an empty string. It's idiomatic to just say "if variable:" rather than "if variable != ''". --- PinetimeFlasher.pyw | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index 5f98059..87c8da5 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -149,7 +149,7 @@ class ptflasher(QMainWindow): filter="PineTime Firmware (*.bin *.hex)", ) - if datafile[0] != "": + if datafile[0]: self.filedir.setPlainText(datafile[0]) self.progress.setValue(0) From d576e8eb4f29d7cd9e048ef304bb0d9e5922df2c Mon Sep 17 00:00:00 2001 From: Patsy Date: Tue, 13 Apr 2021 22:29:55 +0200 Subject: [PATCH 29/47] Remove unintended commits. --- .idea/vcs.xml | 6 -- .idea/workspace.xml | 178 -------------------------------------------- 2 files changed, 184 deletions(-) delete mode 100644 .idea/vcs.xml delete mode 100644 .idea/workspace.xml diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml deleted file mode 100644 index 92a4039..0000000 --- a/.idea/workspace.xml +++ /dev/null @@ -1,178 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - 1618207563887 - - - 1618210793437 - - - 1618211745258 - - - 1618212267211 - - - 1618212351907 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - file://$PROJECT_DIR$/PinetimeFlasher.pyw - 219 - - - file://$PROJECT_DIR$/PinetimeFlasher.pyw - 226 - - - - - \ No newline at end of file From 2b4d91fab59a17b3c053e72cf9a991fb12c11015 Mon Sep 17 00:00:00 2001 From: Peter Feerick Date: Thu, 15 Apr 2021 10:51:45 +1000 Subject: [PATCH 30/47] More lowercase / minor word changes --- PinetimeFlasher.pyw | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index 87c8da5..d5244b4 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -61,7 +61,7 @@ class ptflasher(QMainWindow): self.progress.setValue(0) self.flashbtn = QPushButton("Start flashing") - self.searchbtn = QPushButton("Search for File") + self.searchbtn = QPushButton("Search for file") self.confbtn = QPushButton("Configure flashing options...") self.flashbtn.clicked.connect(self.startflash) self.searchbtn.clicked.connect(self.filesearch) @@ -243,7 +243,7 @@ class InfoDialog(QDialog): textView = QLabel( """PineTime Flasher is a simple GUI software written in Python, that uses the xpack-openOCD tool for flashing the PineTime -with ST-Link, J-Link etc. +with SWD debuggers such as the ST-Link and J-Link. When first using the software, it is recommended that you setup the configuration by choosing the appropriate firmware From 64ab1f549c91eb09d7223084f79b335da1f65c35 Mon Sep 17 00:00:00 2001 From: Peter Feerick Date: Thu, 15 Apr 2021 10:53:54 +1000 Subject: [PATCH 31/47] Disable buttons during flashing As it makes no sense for the user to be search for a file or be in the configuration dialog at this time. --- PinetimeFlasher.pyw | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index d5244b4..1a3d0e9 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -110,6 +110,10 @@ class ptflasher(QMainWindow): self.progress.setValue(0) return + self.searchbtn.setEnabled(False) + self.flashbtn.setEnabled(False) + self.confbtn.setEnabled(False) + self.status.setText("Flashing...") self.status.repaint() @@ -130,8 +134,13 @@ class ptflasher(QMainWindow): else: self.status.setText("Something probably went wrong :(") self.progress.setValue(0) + self.p = None + self.searchbtn.setEnabled(True) + self.flashbtn.setEnabled(True) + self.confbtn.setEnabled(True) + def handle_stderr(self): data = self.p.readAllStandardError() stderr = bytes(data).decode("utf8") From 7886335cf893224eec7f6b0625f35325a93f054b Mon Sep 17 00:00:00 2001 From: Patsy Date: Thu, 15 Apr 2021 21:11:26 +0200 Subject: [PATCH 32/47] Disable flash button until we firmware an paths are validated. --- PinetimeFlasher.pyw | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index 1a3d0e9..bc6520a 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -59,6 +59,7 @@ class ptflasher(QMainWindow): self.progress.setMinimum(0) self.progress.setMaximum(100) self.progress.setValue(0) + self.progress.setEnabled(False) self.flashbtn = QPushButton("Start flashing") self.searchbtn = QPushButton("Search for file") @@ -66,6 +67,8 @@ class ptflasher(QMainWindow): self.flashbtn.clicked.connect(self.startflash) self.searchbtn.clicked.connect(self.filesearch) self.confbtn.clicked.connect(self.confButton) + self.filedir.textChanged.connect(self.update_control_statuses) + self.flashbtn.setEnabled(False) self.status = QLabel("Ready.") @@ -84,6 +87,22 @@ class ptflasher(QMainWindow): self.setCentralWidget(w) + def update_control_statuses(self): + def enable_buttons(enable: bool, reason: str): + self.flashbtn.setEnabled(enable) + self.progress.setEnabled(enable) + self.status.setText(reason) + + firmware = self.filedir.toPlainText() + if not firmware: + enable_buttons(False, "Set location of file to be flashed!") + elif not os.path.exists(firmware): + enable_buttons(False, "File does not exist!") + elif not shutil.which("openocd"): + enable_buttons(False, "OpenOCD not found in system path!") + else: + enable_buttons(True, "Ready to flash!") + def startflash(self): if self.p: # if process is already running return @@ -93,27 +112,10 @@ class ptflasher(QMainWindow): source = self.filedir.toPlainText() address, interface = read_config_file(self.status) - self.progress.setValue(10) - - if source == "": - self.status.setText("Set location of file to be flashed!") - self.progress.setValue(0) - return - - if not os.path.exists(source): - self.status.setText("File does not exist!") - self.progress.setValue(0) - return - - if not shutil.which("openocd"): - self.status.setText("OpenOCD not found in system path!") - self.progress.setValue(0) - return - self.searchbtn.setEnabled(False) - self.flashbtn.setEnabled(False) self.confbtn.setEnabled(False) + self.progress.setValue(10) self.status.setText("Flashing...") self.status.repaint() @@ -138,7 +140,6 @@ class ptflasher(QMainWindow): self.p = None self.searchbtn.setEnabled(True) - self.flashbtn.setEnabled(True) self.confbtn.setEnabled(True) def handle_stderr(self): From eaf922b8473580880c8b3f8ae3d32fb031e2f757 Mon Sep 17 00:00:00 2001 From: Patsy Date: Thu, 15 Apr 2021 21:12:48 +0200 Subject: [PATCH 33/47] Exclude IntelliJ IDEA directory. --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 2672a86..537bd3d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,9 @@ # Program config file conf.dat -# VSCode workspace settings +# IDE workspace settings .vscode/ +.idea/ # Byte-compiled / optimized / DLL files __pycache__/ From 6bdcba643f8b00fc7a7829c494fe0cf2c88af735 Mon Sep 17 00:00:00 2001 From: Patsy Date: Sat, 17 Apr 2021 11:44:10 +0200 Subject: [PATCH 34/47] Move info button to the main dialog, to keep the config dialog focused. --- PinetimeFlasher.pyw | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index bc6520a..d53c9b4 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -52,7 +52,6 @@ class ptflasher(QMainWindow): self.resize(300, 200) self.info = QLabel("Enter the path of the file to be flashed") - self.filedir = QPlainTextEdit() self.progress = QProgressBar() @@ -64,22 +63,24 @@ class ptflasher(QMainWindow): self.flashbtn = QPushButton("Start flashing") self.searchbtn = QPushButton("Search for file") self.confbtn = QPushButton("Configure flashing options...") + self.infobtn = QPushButton("More info") + self.status = QLabel("Ready.") + self.flashbtn.clicked.connect(self.startflash) self.searchbtn.clicked.connect(self.filesearch) self.confbtn.clicked.connect(self.confButton) self.filedir.textChanged.connect(self.update_control_statuses) + self.infobtn.clicked.connect(self.info_button) self.flashbtn.setEnabled(False) - self.status = QLabel("Ready.") - layout = QVBoxLayout() - layout.addWidget(self.info) layout.addWidget(self.filedir) layout.addWidget(self.progress) layout.addWidget(self.searchbtn) layout.addWidget(self.flashbtn) layout.addWidget(self.confbtn) + layout.addWidget(self.infobtn) layout.addWidget(self.status) w = QWidget() @@ -167,6 +168,10 @@ class ptflasher(QMainWindow): dlg = ConfDialog() dlg.exec() + def info_button(self): + dlg = InfoDialog() + dlg.exec() + # Configuration class and UI class ConfDialog(QDialog): @@ -190,7 +195,6 @@ class ConfDialog(QDialog): self.ifacebox = QPlainTextEdit() self.savebtn = QPushButton("Save configuration") - self.infobtn = QPushButton("More info") self.status = QLabel("") @@ -201,11 +205,7 @@ class ConfDialog(QDialog): conflayout.addWidget(self.addrbox) conflayout.addWidget(self.ifaceinfo) conflayout.addWidget(self.ifacebox) - - confbuttonrow.addWidget(self.savebtn) - confbuttonrow.addWidget(self.infobtn) - - conflayout.addLayout(confbuttonrow) + conflayout.addWidget(self.savebtn) conflayout.addWidget(self.status) self.setLayout(conflayout) @@ -214,7 +214,6 @@ class ConfDialog(QDialog): self.addrbox.setCurrentIndex(self.get_firmware_index(address)) self.ifacebox.setPlainText(interface) - self.infobtn.clicked.connect(self.infoButton) self.savebtn.clicked.connect(self.saveconf) self.setWindowModality(Qt.ApplicationModal) @@ -236,10 +235,6 @@ class ConfDialog(QDialog): except OSError: self.status.setText("Unable to write configuration file!") - def infoButton(self, s): - dlg = InfoDialog() - dlg.exec() - # Info screen class and UI class InfoDialog(QDialog): From 560a673bc4a97391d36dd5e81798a941363bf3e2 Mon Sep 17 00:00:00 2001 From: Patsy Date: Sat, 17 Apr 2021 15:52:37 +0200 Subject: [PATCH 35/47] Add 'Download OpenOCD' button to configuration settings that downloads and unpacks the latest OpenOCD release from GitHub. --- PinetimeFlasher.pyw | 98 +++++++++++++++++++++++++++++++++++++++++++-- requirements.txt | 3 ++ 2 files changed, 98 insertions(+), 3 deletions(-) diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index d53c9b4..cabb992 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -1,13 +1,24 @@ #!/usr/bin/env python3 - +import hashlib +import platform import sys import os import shutil +import requests +import json +import wget from pathlib import Path from PyQt5.QtCore import * from PyQt5.QtWidgets import * from PyQt5.QtGui import * import pickle +from typing import List + + +def add_openocd_to_system_path(status_notice: QLabel): + openocd_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'openocd', 'bin') + os.environ["PATH"] = os.environ["PATH"] + os.pathsep + openocd_path + status_notice.setText("OpenOCD downloaded and added to system path.") # Returns percentage progress value in response to key phrases from OpenOCD @@ -87,6 +98,8 @@ class ptflasher(QMainWindow): w.setLayout(layout) self.setCentralWidget(w) + if os.path.exists('openocd'): + add_openocd_to_system_path(self.status) def update_control_statuses(self): def enable_buttons(enable: bool, reason: str): @@ -184,7 +197,7 @@ class ConfDialog(QDialog): ] self.setWindowTitle("Flash Configuration") - self.resize(300, 200) + self.resize(300, 220) self.addrinfo = QLabel("Firmware type (used to determine address):") self.addrbox = QComboBox() @@ -195,7 +208,7 @@ class ConfDialog(QDialog): self.ifacebox = QPlainTextEdit() self.savebtn = QPushButton("Save configuration") - + self.openocd_btn = QPushButton("Download OpenOCD") self.status = QLabel("") conflayout = QVBoxLayout() @@ -206,6 +219,7 @@ class ConfDialog(QDialog): conflayout.addWidget(self.ifaceinfo) conflayout.addWidget(self.ifacebox) conflayout.addWidget(self.savebtn) + conflayout.addWidget(self.openocd_btn) conflayout.addWidget(self.status) self.setLayout(conflayout) @@ -215,6 +229,7 @@ class ConfDialog(QDialog): self.ifacebox.setPlainText(interface) self.savebtn.clicked.connect(self.saveconf) + self.openocd_btn.clicked.connect(self.setup_openocd) self.setWindowModality(Qt.ApplicationModal) @@ -235,6 +250,83 @@ class ConfDialog(QDialog): except OSError: self.status.setText("Unable to write configuration file!") + def setup_openocd(self): + """ + Download and unpack OpenOCD for the current platform, then add it to the system path. + """ + self.status.setText("Finding latest OpenOCD release...") + + base_url = "https://api.github.com/repos" + response = requests.get(f"{base_url}/xpack-dev-tools/openocd-xpack/releases/latest") + + details = response.content.decode('utf-8') + content = json.loads(details) + + archive_file, hash_file = self.get_github_assets(content["assets"]) + computed_hash, provided_hash = self.get_hashes(archive_file, hash_file) + if computed_hash != provided_hash: + self.status.setText("Hashes do not match - corrupted download!") + return + + self.unpack_archive(archive_file) + add_openocd_to_system_path(self.status) + os.remove(archive_file) + os.remove(hash_file) + + def get_hashes(self, archive_file, hash_file): + self.status.setText("Computing hashes OpenOCD...") + with open(archive_file, "rb") as fd: + hasher = hashlib.sha256() + hasher.update(fd.read()) + computed_hash = hasher.hexdigest() + with open(hash_file, "r") as fd: + provided_hash= fd.read().split(" ")[0] # format: " " + return computed_hash, provided_hash + + def unpack_archive(self, archive): + """ + Unpack the archive and shift files so that the files can always be found + under 'openocd/' (i.e. without a version number, which is how they are + currently packed). + """ + self.status.setText("Unpacking OpenOCD...") + tmpdir_name = "openocd_tmp" + shutil.unpack_archive(archive, extract_dir=tmpdir_name) + tmpdir_contents = os.listdir(tmpdir_name) + + if len(tmpdir_contents) == 1: + shutil.move(os.path.join(tmpdir_name, tmpdir_contents[0]), "openocd") + os.rmdir(tmpdir_name) + else: + shutil.move(tmpdir_name, "openocd") + + def get_github_assets(self, assets: List[str]): + plat = { + "Windows": "win32", + "Linux": "linux", + "MacOS": "darwin", + }.get(platform.system(), "") + arch = { + "x86_64": "x64", + "i386": "ia32" + }.get(platform.machine(), "") + + if not plat or not arch: + self.status.setText("Unable to determine appropriate OpenOCD download.") + return + + download_urls = [f["browser_download_url"] for f in assets if plat in f["name"] and arch in f["name"]] + filenames = [f["name"] for f in assets if plat in f["name"] and arch in f["name"]] + assert len(download_urls) == 2 + + self.status.setText("Downloading OpenOCD from GitHub...") + if not os.path.exists(filenames[0]) and not os.path.exists(filenames[1]): + wget.download(download_urls[0]) + wget.download(download_urls[1]) + + filenames.sort() + return filenames + # Info screen class and UI class InfoDialog(QDialog): diff --git a/requirements.txt b/requirements.txt index 656741a..f8a64f0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,6 @@ PyQt5==5.15.4 +PyQt5-stubs==5.15.2 PyQt5-Qt5==5.15.2 PyQt5-sip==12.8.1 +wget +requests \ No newline at end of file From 58d3384d5d37260959bee3b8ccf2f2416e6f985a Mon Sep 17 00:00:00 2001 From: Patsy Date: Sat, 17 Apr 2021 15:59:48 +0200 Subject: [PATCH 36/47] Add openocd files to gitignore. --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 537bd3d..2b4f101 100644 --- a/.gitignore +++ b/.gitignore @@ -142,3 +142,8 @@ dmypy.json # Cython debug symbols cython_debug/ + +# OpenOCD files and directories +openocd/ +openocd_tmp/ +openocd-* From aa2f047a1cb3244a13b563f23ca097632f8b8233 Mon Sep 17 00:00:00 2001 From: Patsy Date: Sun, 18 Apr 2021 18:59:22 +0200 Subject: [PATCH 37/47] Add OpenOCD to system path on startup, irrespective of whether it's present or not. If it isn't present, this won't hurt, and if it is, we're just setting it up sooner. --- PinetimeFlasher.pyw | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index cabb992..4a6318e 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -15,10 +15,9 @@ import pickle from typing import List -def add_openocd_to_system_path(status_notice: QLabel): +def add_openocd_to_system_path(): openocd_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'openocd', 'bin') os.environ["PATH"] = os.environ["PATH"] + os.pathsep + openocd_path - status_notice.setText("OpenOCD downloaded and added to system path.") # Returns percentage progress value in response to key phrases from OpenOCD @@ -98,8 +97,6 @@ class ptflasher(QMainWindow): w.setLayout(layout) self.setCentralWidget(w) - if os.path.exists('openocd'): - add_openocd_to_system_path(self.status) def update_control_statuses(self): def enable_buttons(enable: bool, reason: str): @@ -212,8 +209,6 @@ class ConfDialog(QDialog): self.status = QLabel("") conflayout = QVBoxLayout() - confbuttonrow = QHBoxLayout() - conflayout.addWidget(self.addrinfo) conflayout.addWidget(self.addrbox) conflayout.addWidget(self.ifaceinfo) @@ -221,7 +216,6 @@ class ConfDialog(QDialog): conflayout.addWidget(self.savebtn) conflayout.addWidget(self.openocd_btn) conflayout.addWidget(self.status) - self.setLayout(conflayout) address, interface = read_config_file(self.status) @@ -269,7 +263,7 @@ class ConfDialog(QDialog): return self.unpack_archive(archive_file) - add_openocd_to_system_path(self.status) + self.status.setText("OpenOCD successfully downloaded.") os.remove(archive_file) os.remove(hash_file) @@ -377,6 +371,8 @@ if __name__ == "__main__": qp.setColor(QPalette.Button, Qt.gray) app.setPalette(qp) + add_openocd_to_system_path() + win = ptflasher() win.show() sys.exit(app.exec_()) From 9cecb273f6703da4c335bd8c67bf3a80bad06360 Mon Sep 17 00:00:00 2001 From: Patsy Date: Sun, 18 Apr 2021 19:00:05 +0200 Subject: [PATCH 38/47] On Windows 10 x64, platform.machine() returns AMD64, so take this into account. --- PinetimeFlasher.pyw | 1 + 1 file changed, 1 insertion(+) diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index 4a6318e..ab3f6c9 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -301,6 +301,7 @@ class ConfDialog(QDialog): "MacOS": "darwin", }.get(platform.system(), "") arch = { + "AMD64": "x64", "x86_64": "x64", "i386": "ia32" }.get(platform.machine(), "") From 7c1ca8069b63e21454a44db50ad511fa3fe925a5 Mon Sep 17 00:00:00 2001 From: Patsy Date: Sun, 18 Apr 2021 19:00:22 +0200 Subject: [PATCH 39/47] Handle download errors before trying to compute hashes. --- PinetimeFlasher.pyw | 12 +++++++----- requirements.txt | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index ab3f6c9..be7c5a7 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -257,8 +257,10 @@ class ConfDialog(QDialog): content = json.loads(details) archive_file, hash_file = self.get_github_assets(content["assets"]) - computed_hash, provided_hash = self.get_hashes(archive_file, hash_file) - if computed_hash != provided_hash: + if not archive_file or not hash_file: + return + + if not self.compare_hashes(archive_file, hash_file): self.status.setText("Hashes do not match - corrupted download!") return @@ -267,15 +269,15 @@ class ConfDialog(QDialog): os.remove(archive_file) os.remove(hash_file) - def get_hashes(self, archive_file, hash_file): + def compare_hashes(self, archive_file, hash_file): self.status.setText("Computing hashes OpenOCD...") with open(archive_file, "rb") as fd: hasher = hashlib.sha256() hasher.update(fd.read()) computed_hash = hasher.hexdigest() with open(hash_file, "r") as fd: - provided_hash= fd.read().split(" ")[0] # format: " " - return computed_hash, provided_hash + provided_hash = fd.read().split(" ")[0] # format: " " + return computed_hash == provided_hash def unpack_archive(self, archive): """ diff --git a/requirements.txt b/requirements.txt index f8a64f0..11f89fe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ PyQt5==5.15.4 -PyQt5-stubs==5.15.2 +PyQt5-stubs==5.15.2.0 PyQt5-Qt5==5.15.2 PyQt5-sip==12.8.1 wget From 69925c243959494dcfcf1eed2ae9637952d3f6b7 Mon Sep 17 00:00:00 2001 From: Peter Feerick Date: Mon, 19 Apr 2021 13:06:45 +1000 Subject: [PATCH 40/47] Update the main dialog status when exit config So that if you just downloaded OpenOCD, the app will update the current ready status. --- PinetimeFlasher.pyw | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index be7c5a7..eb2dd6d 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -74,7 +74,7 @@ class ptflasher(QMainWindow): self.searchbtn = QPushButton("Search for file") self.confbtn = QPushButton("Configure flashing options...") self.infobtn = QPushButton("More info") - self.status = QLabel("Ready.") + self.status = QLabel("") self.flashbtn.clicked.connect(self.startflash) self.searchbtn.clicked.connect(self.filesearch) @@ -177,6 +177,7 @@ class ptflasher(QMainWindow): def confButton(self, s): dlg = ConfDialog() dlg.exec() + self.update_control_statuses() def info_button(self): dlg = InfoDialog() From bc38a3a73d3bc2ce74d978c3800089eccfae6be7 Mon Sep 17 00:00:00 2001 From: Peter Feerick Date: Mon, 19 Apr 2021 16:12:31 +1000 Subject: [PATCH 41/47] Disable flash button whilst flashing --- PinetimeFlasher.pyw | 2 ++ 1 file changed, 2 insertions(+) diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index eb2dd6d..b199d39 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -124,6 +124,7 @@ class ptflasher(QMainWindow): address, interface = read_config_file(self.status) self.searchbtn.setEnabled(False) + self.flashbtn.setEnabled(False) self.confbtn.setEnabled(False) self.progress.setValue(10) @@ -151,6 +152,7 @@ class ptflasher(QMainWindow): self.p = None self.searchbtn.setEnabled(True) + self.flashbtn.setEnabled(True) self.confbtn.setEnabled(True) def handle_stderr(self): From 26d42a51f4816247b6003dd34d07042b3effa60d Mon Sep 17 00:00:00 2001 From: Peter Feerick Date: Mon, 19 Apr 2021 16:14:01 +1000 Subject: [PATCH 42/47] Add corrected upstream drag and drop As upstream didn't taking into account OS path variations --- PinetimeFlasher.pyw | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index b199d39..7531c3a 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -105,10 +105,22 @@ class ptflasher(QMainWindow): self.status.setText(reason) firmware = self.filedir.toPlainText() + + if platform.system() == "Windows": + if firmware[0:8] == "file:///": + firmware = firmware[8:] + self.filedir.setPlainText(firmware) + else: + if firmware[0:7] == "file://": + firmware = firmware[7:] + self.filedir.setPlainText(firmware) + if not firmware: enable_buttons(False, "Set location of file to be flashed!") elif not os.path.exists(firmware): enable_buttons(False, "File does not exist!") + elif not os.path.splitext(firmware)[-1] in (".bin", ".hex"): + enable_buttons(False, "Not a supported file type (.bin, .hex)") elif not shutil.which("openocd"): enable_buttons(False, "OpenOCD not found in system path!") else: From 6378017da1b0bab40b35ea6f2092e2b9f760bb0f Mon Sep 17 00:00:00 2001 From: Peter Feerick Date: Mon, 19 Apr 2021 16:20:31 +1000 Subject: [PATCH 43/47] Force status text repaint for quick-updating tasks --- PinetimeFlasher.pyw | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index 7531c3a..4a06936 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -264,6 +264,7 @@ class ConfDialog(QDialog): Download and unpack OpenOCD for the current platform, then add it to the system path. """ self.status.setText("Finding latest OpenOCD release...") + self.status.repaint() base_url = "https://api.github.com/repos" response = requests.get(f"{base_url}/xpack-dev-tools/openocd-xpack/releases/latest") @@ -286,6 +287,7 @@ class ConfDialog(QDialog): def compare_hashes(self, archive_file, hash_file): self.status.setText("Computing hashes OpenOCD...") + self.status.repaint() with open(archive_file, "rb") as fd: hasher = hashlib.sha256() hasher.update(fd.read()) @@ -301,6 +303,7 @@ class ConfDialog(QDialog): currently packed). """ self.status.setText("Unpacking OpenOCD...") + self.status.repaint() tmpdir_name = "openocd_tmp" shutil.unpack_archive(archive, extract_dir=tmpdir_name) tmpdir_contents = os.listdir(tmpdir_name) @@ -332,6 +335,7 @@ class ConfDialog(QDialog): assert len(download_urls) == 2 self.status.setText("Downloading OpenOCD from GitHub...") + self.status.repaint() if not os.path.exists(filenames[0]) and not os.path.exists(filenames[1]): wget.download(download_urls[0]) wget.download(download_urls[1]) From 55560798547e5f2743b24529b4e99189d66a8669 Mon Sep 17 00:00:00 2001 From: Peter Feerick Date: Mon, 19 Apr 2021 19:02:26 +1000 Subject: [PATCH 44/47] Add version number to app --- PinetimeFlasher.pyw | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index 4a06936..bb74533 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -14,6 +14,8 @@ from PyQt5.QtGui import * import pickle from typing import List +__version__ = "0.4.0" + def add_openocd_to_system_path(): openocd_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'openocd', 'bin') @@ -349,12 +351,11 @@ class InfoDialog(QDialog): def __init__(self, parent=None): super().__init__(parent=parent) - self.setWindowTitle("About PineTime Flasher") + self.setWindowTitle("About PineTime Flasher v{}".format(__version__)) self.resize(300, 200) vbox = QVBoxLayout() - textView = QLabel( -"""PineTime Flasher is a simple GUI software written in Python, + textView = QLabel("""PineTime Flasher is a simple GUI software written in Python, that uses the xpack-openOCD tool for flashing the PineTime with SWD debuggers such as the ST-Link and J-Link. From ea8d1db90e2d904115baa75b6b7bbdf5e1e7c0d1 Mon Sep 17 00:00:00 2001 From: Peter Feerick Date: Mon, 19 Apr 2021 19:23:04 +1000 Subject: [PATCH 45/47] Tidying up code comments and add a few more --- PinetimeFlasher.pyw | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index bb74533..a7b59f1 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -18,12 +18,17 @@ __version__ = "0.4.0" def add_openocd_to_system_path(): + """ + Adds the directory 'openocd' to the path, relative to the app + """ openocd_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'openocd', 'bin') os.environ["PATH"] = os.environ["PATH"] + os.pathsep + openocd_path -# Returns percentage progress value in response to key phrases from OpenOCD def progress_parser(output): + """ + Returns percentage progress value in response to key phrases from OpenOCD + """ if "** Programming Started **" in output: return 30 elif "** Programming Finished **" in output: @@ -53,8 +58,10 @@ def read_config_file(status_notice: QLabel) -> (str, str): return "0x00008000", "stlink.cfg" -# Main Program Class and UI class ptflasher(QMainWindow): + """ + Main Program Class and UI + """ def __init__(self): super().__init__() @@ -101,6 +108,9 @@ class ptflasher(QMainWindow): self.setCentralWidget(w) def update_control_statuses(self): + """ + Enable or disable buttons and update status messages as needed + """ def enable_buttons(enable: bool, reason: str): self.flashbtn.setEnabled(enable) self.progress.setEnabled(enable) @@ -129,7 +139,10 @@ class ptflasher(QMainWindow): enable_buttons(True, "Ready to flash!") def startflash(self): - if self.p: # if process is already running + """ + Start the actual flashing process + """ + if self.p: # don't continue if process already running return self.progress.setValue(0) @@ -157,6 +170,9 @@ class ptflasher(QMainWindow): self.p.start(command) def flash_finished(self): + """ + Handle flash completion + """ if self.p.exitCode() == 0: self.status.setText("Success!") else: @@ -170,6 +186,9 @@ class ptflasher(QMainWindow): self.confbtn.setEnabled(True) def handle_stderr(self): + """ + Capture output during flashing and process it + """ data = self.p.readAllStandardError() stderr = bytes(data).decode("utf8") progress = progress_parser(stderr) @@ -200,8 +219,10 @@ class ptflasher(QMainWindow): dlg.exec() -# Configuration class and UI class ConfDialog(QDialog): + """ + Configuration class and UI + """ def __init__(self, parent=None): super().__init__(parent=parent) @@ -317,6 +338,10 @@ class ConfDialog(QDialog): shutil.move(tmpdir_name, "openocd") def get_github_assets(self, assets: List[str]): + """ + Determine which package needs to be downloaded for OS/architecture + and attempt to download it. + """ plat = { "Windows": "win32", "Linux": "linux", @@ -346,8 +371,10 @@ class ConfDialog(QDialog): return filenames -# Info screen class and UI class InfoDialog(QDialog): + """ + Info screen class and UI + """ def __init__(self, parent=None): super().__init__(parent=parent) @@ -376,7 +403,6 @@ stlink.cfg or jlink.cfg""") self.setWindowModality(Qt.ApplicationModal) -# Program entrypoint if __name__ == "__main__": app = QApplication(sys.argv) app.setStyle("Fusion") From f93c817955e1ac8dda742ce051de242f7a49b15c Mon Sep 17 00:00:00 2001 From: Peter Feerick Date: Mon, 19 Apr 2021 19:25:02 +1000 Subject: [PATCH 46/47] Formatting --- PinetimeFlasher.pyw | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index a7b59f1..c269f42 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -62,6 +62,7 @@ class ptflasher(QMainWindow): """ Main Program Class and UI """ + def __init__(self): super().__init__() @@ -223,6 +224,7 @@ class ConfDialog(QDialog): """ Configuration class and UI """ + def __init__(self, parent=None): super().__init__(parent=parent) @@ -375,6 +377,7 @@ class InfoDialog(QDialog): """ Info screen class and UI """ + def __init__(self, parent=None): super().__init__(parent=parent) @@ -397,6 +400,7 @@ The possible firmware types are: For the interface, the options available are dependent on the (*.cfg) provided by the xpack-openOCD itself. For example: stlink.cfg or jlink.cfg""") + vbox.addWidget(textView) self.setLayout(vbox) @@ -407,8 +411,7 @@ if __name__ == "__main__": app = QApplication(sys.argv) app.setStyle("Fusion") - appDir = getattr(sys, "_MEIPASS", - os.path.abspath(os.path.dirname(__file__))) + appDir = getattr(sys, "_MEIPASS", os.path.abspath(os.path.dirname(__file__))) path_to_icon = os.path.abspath(os.path.join(appDir, "PinetimeFlasher.ico")) app_icon = QIcon(path_to_icon) From 8143e3d2c85d0de1107aadab876656943f0d7359 Mon Sep 17 00:00:00 2001 From: Peter Feerick Date: Mon, 19 Apr 2021 20:21:40 +1000 Subject: [PATCH 47/47] Show openocd output if flashing fails Closes #10 --- PinetimeFlasher.pyw | 887 +++++++++++++++++++++++--------------------- 1 file changed, 457 insertions(+), 430 deletions(-) diff --git a/PinetimeFlasher.pyw b/PinetimeFlasher.pyw index c269f42..455697d 100644 --- a/PinetimeFlasher.pyw +++ b/PinetimeFlasher.pyw @@ -1,430 +1,457 @@ -#!/usr/bin/env python3 -import hashlib -import platform -import sys -import os -import shutil -import requests -import json -import wget -from pathlib import Path -from PyQt5.QtCore import * -from PyQt5.QtWidgets import * -from PyQt5.QtGui import * -import pickle -from typing import List - -__version__ = "0.4.0" - - -def add_openocd_to_system_path(): - """ - Adds the directory 'openocd' to the path, relative to the app - """ - openocd_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'openocd', 'bin') - os.environ["PATH"] = os.environ["PATH"] + os.pathsep + openocd_path - - -def progress_parser(output): - """ - Returns percentage progress value in response to key phrases from OpenOCD - """ - if "** Programming Started **" in output: - return 30 - elif "** Programming Finished **" in output: - return 50 - elif "** Verify Started **" in output: - return 70 - elif "** Verified OK **" in output: - return 90 - elif "** Resetting Target **" in output: - return 100 - - -def read_config_file(status_notice: QLabel) -> (str, str): - """ - Return the (address, interface) as read from the config file. - Returns default values for both if there are any file access errors. - """ - try: - with open("conf.dat", "rb+") as f: - data = pickle.load(f) - address = data[0] - interface = data[1] - return address, interface - except OSError: - status_notice.setText("Unable to read config file - using default values!") - - return "0x00008000", "stlink.cfg" - - -class ptflasher(QMainWindow): - """ - Main Program Class and UI - """ - - def __init__(self): - super().__init__() - - self.p = None # Default empty value. - - self.setWindowTitle("PineTime Flasher") - self.resize(300, 200) - - self.info = QLabel("Enter the path of the file to be flashed") - self.filedir = QPlainTextEdit() - - self.progress = QProgressBar() - self.progress.setMinimum(0) - self.progress.setMaximum(100) - self.progress.setValue(0) - self.progress.setEnabled(False) - - self.flashbtn = QPushButton("Start flashing") - self.searchbtn = QPushButton("Search for file") - self.confbtn = QPushButton("Configure flashing options...") - self.infobtn = QPushButton("More info") - self.status = QLabel("") - - self.flashbtn.clicked.connect(self.startflash) - self.searchbtn.clicked.connect(self.filesearch) - self.confbtn.clicked.connect(self.confButton) - self.filedir.textChanged.connect(self.update_control_statuses) - self.infobtn.clicked.connect(self.info_button) - self.flashbtn.setEnabled(False) - - layout = QVBoxLayout() - layout.addWidget(self.info) - layout.addWidget(self.filedir) - layout.addWidget(self.progress) - layout.addWidget(self.searchbtn) - layout.addWidget(self.flashbtn) - layout.addWidget(self.confbtn) - layout.addWidget(self.infobtn) - layout.addWidget(self.status) - - w = QWidget() - w.setLayout(layout) - - self.setCentralWidget(w) - - def update_control_statuses(self): - """ - Enable or disable buttons and update status messages as needed - """ - def enable_buttons(enable: bool, reason: str): - self.flashbtn.setEnabled(enable) - self.progress.setEnabled(enable) - self.status.setText(reason) - - firmware = self.filedir.toPlainText() - - if platform.system() == "Windows": - if firmware[0:8] == "file:///": - firmware = firmware[8:] - self.filedir.setPlainText(firmware) - else: - if firmware[0:7] == "file://": - firmware = firmware[7:] - self.filedir.setPlainText(firmware) - - if not firmware: - enable_buttons(False, "Set location of file to be flashed!") - elif not os.path.exists(firmware): - enable_buttons(False, "File does not exist!") - elif not os.path.splitext(firmware)[-1] in (".bin", ".hex"): - enable_buttons(False, "Not a supported file type (.bin, .hex)") - elif not shutil.which("openocd"): - enable_buttons(False, "OpenOCD not found in system path!") - else: - enable_buttons(True, "Ready to flash!") - - def startflash(self): - """ - Start the actual flashing process - """ - if self.p: # don't continue if process already running - return - - self.progress.setValue(0) - - source = self.filedir.toPlainText() - address, interface = read_config_file(self.status) - - self.searchbtn.setEnabled(False) - self.flashbtn.setEnabled(False) - self.confbtn.setEnabled(False) - - self.progress.setValue(10) - self.status.setText("Flashing...") - self.status.repaint() - - command = ( - 'openocd -f "interface/{}" ' - '-f "target/nrf52.cfg" -c "init" ' - '-c "program {} {} verify reset exit"' - ).format(interface, source, address) - - self.p = QProcess() # Keep a reference while it's running - self.p.finished.connect(self.flash_finished) # Clean up - self.p.readyReadStandardError.connect(self.handle_stderr) - self.p.start(command) - - def flash_finished(self): - """ - Handle flash completion - """ - if self.p.exitCode() == 0: - self.status.setText("Success!") - else: - self.status.setText("Something probably went wrong :(") - self.progress.setValue(0) - - self.p = None - - self.searchbtn.setEnabled(True) - self.flashbtn.setEnabled(True) - self.confbtn.setEnabled(True) - - def handle_stderr(self): - """ - Capture output during flashing and process it - """ - data = self.p.readAllStandardError() - stderr = bytes(data).decode("utf8") - progress = progress_parser(stderr) - if progress: - self.progress.setValue(progress) - if progress == 70: - self.status.setText("Verifying...") - - def filesearch(self): - filedialog = QFileDialog() - datafile = filedialog.getOpenFileName( - caption="Select firmware file to flash...", - directory=str(Path.home() / "Downloads"), - filter="PineTime Firmware (*.bin *.hex)", - ) - - if datafile[0]: - self.filedir.setPlainText(datafile[0]) - self.progress.setValue(0) - - def confButton(self, s): - dlg = ConfDialog() - dlg.exec() - self.update_control_statuses() - - def info_button(self): - dlg = InfoDialog() - dlg.exec() - - -class ConfDialog(QDialog): - """ - Configuration class and UI - """ - - def __init__(self, parent=None): - super().__init__(parent=parent) - - self.firmware_types = [ - {"name": "mcuboot-app", "address": "0x00008000"}, - {"name": "bootloader", "address": "0x00000000"} - ] - - self.setWindowTitle("Flash Configuration") - self.resize(300, 220) - - self.addrinfo = QLabel("Firmware type (used to determine address):") - self.addrbox = QComboBox() - for firmware in self.firmware_types: - self.addrbox.addItem(firmware["name"], firmware["address"]) - - self.ifaceinfo = QLabel("Enter the interface (default: stlink.cfg)") - self.ifacebox = QPlainTextEdit() - - self.savebtn = QPushButton("Save configuration") - self.openocd_btn = QPushButton("Download OpenOCD") - self.status = QLabel("") - - conflayout = QVBoxLayout() - conflayout.addWidget(self.addrinfo) - conflayout.addWidget(self.addrbox) - conflayout.addWidget(self.ifaceinfo) - conflayout.addWidget(self.ifacebox) - conflayout.addWidget(self.savebtn) - conflayout.addWidget(self.openocd_btn) - conflayout.addWidget(self.status) - self.setLayout(conflayout) - - address, interface = read_config_file(self.status) - self.addrbox.setCurrentIndex(self.get_firmware_index(address)) - self.ifacebox.setPlainText(interface) - - self.savebtn.clicked.connect(self.saveconf) - self.openocd_btn.clicked.connect(self.setup_openocd) - - self.setWindowModality(Qt.ApplicationModal) - - def get_firmware_index(self, address: str): - for i, firmware in enumerate(self.firmware_types): - if firmware["address"] == address: - return i - return 0 - - def saveconf(self, s): - addr = self.addrbox.currentData() - iface = self.ifacebox.toPlainText() or "stlink.cfg" - - try: - with open("conf.dat", "wb+") as f: - pickle.dump((addr, iface), f) - self.status.setText("Configuration Saved.") - except OSError: - self.status.setText("Unable to write configuration file!") - - def setup_openocd(self): - """ - Download and unpack OpenOCD for the current platform, then add it to the system path. - """ - self.status.setText("Finding latest OpenOCD release...") - self.status.repaint() - - base_url = "https://api.github.com/repos" - response = requests.get(f"{base_url}/xpack-dev-tools/openocd-xpack/releases/latest") - - details = response.content.decode('utf-8') - content = json.loads(details) - - archive_file, hash_file = self.get_github_assets(content["assets"]) - if not archive_file or not hash_file: - return - - if not self.compare_hashes(archive_file, hash_file): - self.status.setText("Hashes do not match - corrupted download!") - return - - self.unpack_archive(archive_file) - self.status.setText("OpenOCD successfully downloaded.") - os.remove(archive_file) - os.remove(hash_file) - - def compare_hashes(self, archive_file, hash_file): - self.status.setText("Computing hashes OpenOCD...") - self.status.repaint() - with open(archive_file, "rb") as fd: - hasher = hashlib.sha256() - hasher.update(fd.read()) - computed_hash = hasher.hexdigest() - with open(hash_file, "r") as fd: - provided_hash = fd.read().split(" ")[0] # format: " " - return computed_hash == provided_hash - - def unpack_archive(self, archive): - """ - Unpack the archive and shift files so that the files can always be found - under 'openocd/' (i.e. without a version number, which is how they are - currently packed). - """ - self.status.setText("Unpacking OpenOCD...") - self.status.repaint() - tmpdir_name = "openocd_tmp" - shutil.unpack_archive(archive, extract_dir=tmpdir_name) - tmpdir_contents = os.listdir(tmpdir_name) - - if len(tmpdir_contents) == 1: - shutil.move(os.path.join(tmpdir_name, tmpdir_contents[0]), "openocd") - os.rmdir(tmpdir_name) - else: - shutil.move(tmpdir_name, "openocd") - - def get_github_assets(self, assets: List[str]): - """ - Determine which package needs to be downloaded for OS/architecture - and attempt to download it. - """ - plat = { - "Windows": "win32", - "Linux": "linux", - "MacOS": "darwin", - }.get(platform.system(), "") - arch = { - "AMD64": "x64", - "x86_64": "x64", - "i386": "ia32" - }.get(platform.machine(), "") - - if not plat or not arch: - self.status.setText("Unable to determine appropriate OpenOCD download.") - return - - download_urls = [f["browser_download_url"] for f in assets if plat in f["name"] and arch in f["name"]] - filenames = [f["name"] for f in assets if plat in f["name"] and arch in f["name"]] - assert len(download_urls) == 2 - - self.status.setText("Downloading OpenOCD from GitHub...") - self.status.repaint() - if not os.path.exists(filenames[0]) and not os.path.exists(filenames[1]): - wget.download(download_urls[0]) - wget.download(download_urls[1]) - - filenames.sort() - return filenames - - -class InfoDialog(QDialog): - """ - Info screen class and UI - """ - - def __init__(self, parent=None): - super().__init__(parent=parent) - - self.setWindowTitle("About PineTime Flasher v{}".format(__version__)) - self.resize(300, 200) - - vbox = QVBoxLayout() - textView = QLabel("""PineTime Flasher is a simple GUI software written in Python, -that uses the xpack-openOCD tool for flashing the PineTime -with SWD debuggers such as the ST-Link and J-Link. - -When first using the software, it is recommended that you -setup the configuration by choosing the appropriate firmware -type and flashing interface. - -The possible firmware types are: -* mcuboot-app -* bootloader - -For the interface, the options available are dependent on the -(*.cfg) provided by the xpack-openOCD itself. For example: -stlink.cfg or jlink.cfg""") - - vbox.addWidget(textView) - self.setLayout(vbox) - - self.setWindowModality(Qt.ApplicationModal) - - -if __name__ == "__main__": - app = QApplication(sys.argv) - app.setStyle("Fusion") - - appDir = getattr(sys, "_MEIPASS", os.path.abspath(os.path.dirname(__file__))) - path_to_icon = os.path.abspath(os.path.join(appDir, "PinetimeFlasher.ico")) - - app_icon = QIcon(path_to_icon) - app.setWindowIcon(app_icon) - - qp = QPalette() - qp.setColor(QPalette.ButtonText, Qt.white) - qp.setColor(QPalette.Window, Qt.gray) - qp.setColor(QPalette.Button, Qt.gray) - app.setPalette(qp) - - add_openocd_to_system_path() - - win = ptflasher() - win.show() - sys.exit(app.exec_()) +#!/usr/bin/env python3 +import hashlib +import platform +import sys +import os +import shutil +import requests +import json +import wget +from pathlib import Path +from PyQt5.QtCore import * +from PyQt5.QtWidgets import * +from PyQt5.QtGui import * +import pickle +from typing import List + +__version__ = "0.4.0" + + +def add_openocd_to_system_path(): + """ + Adds the directory 'openocd' to the path, relative to the app + """ + openocd_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'openocd', 'bin') + os.environ["PATH"] = os.environ["PATH"] + os.pathsep + openocd_path + + +def progress_parser(output): + """ + Returns percentage progress value in response to key phrases from OpenOCD + """ + if "** Programming Started **" in output: + return 30 + elif "** Programming Finished **" in output: + return 50 + elif "** Verify Started **" in output: + return 70 + elif "** Verified OK **" in output: + return 90 + elif "** Resetting Target **" in output: + return 100 + + +def read_config_file(status_notice: QLabel) -> (str, str): + """ + Return the (address, interface) as read from the config file. + Returns default values for both if there are any file access errors. + """ + try: + with open("conf.dat", "rb+") as f: + data = pickle.load(f) + address = data[0] + interface = data[1] + return address, interface + except OSError: + status_notice.setText("Unable to read config file - using default values!") + + return "0x00008000", "stlink.cfg" + + +class ptflasher(QMainWindow): + """ + Main Program Class and UI + """ + openocd_log = "" + + def __init__(self): + super().__init__() + + self.p = None # Default empty value. + + self.setWindowTitle("PineTime Flasher") + self.resize(300, 200) + + self.info = QLabel("Enter the path of the file to be flashed") + self.filedir = QPlainTextEdit() + + self.progress = QProgressBar() + self.progress.setMinimum(0) + self.progress.setMaximum(100) + self.progress.setValue(0) + self.progress.setEnabled(False) + + self.flashbtn = QPushButton("Start flashing") + self.searchbtn = QPushButton("Search for file") + self.confbtn = QPushButton("Configure flashing options...") + self.infobtn = QPushButton("More info") + self.status = QLabel("") + + self.flashbtn.clicked.connect(self.startflash) + self.searchbtn.clicked.connect(self.filesearch) + self.confbtn.clicked.connect(self.confButton) + self.filedir.textChanged.connect(self.update_control_statuses) + self.infobtn.clicked.connect(self.info_button) + self.flashbtn.setEnabled(False) + + layout = QVBoxLayout() + layout.addWidget(self.info) + layout.addWidget(self.filedir) + layout.addWidget(self.progress) + layout.addWidget(self.searchbtn) + layout.addWidget(self.flashbtn) + layout.addWidget(self.confbtn) + layout.addWidget(self.infobtn) + layout.addWidget(self.status) + + w = QWidget() + w.setLayout(layout) + + self.setCentralWidget(w) + + def update_control_statuses(self): + """ + Enable or disable buttons and update status messages as needed + """ + def enable_buttons(enable: bool, reason: str): + self.flashbtn.setEnabled(enable) + self.progress.setEnabled(enable) + self.status.setText(reason) + + firmware = self.filedir.toPlainText() + + if platform.system() == "Windows": + if firmware[0:8] == "file:///": + firmware = firmware[8:] + self.filedir.setPlainText(firmware) + else: + if firmware[0:7] == "file://": + firmware = firmware[7:] + self.filedir.setPlainText(firmware) + + if not firmware: + enable_buttons(False, "Set location of file to be flashed!") + elif not os.path.exists(firmware): + enable_buttons(False, "File does not exist!") + elif not os.path.splitext(firmware)[-1] in (".bin", ".hex"): + enable_buttons(False, "Not a supported file type (.bin, .hex)") + elif not shutil.which("openocd"): + enable_buttons(False, "OpenOCD not found in system path!") + else: + enable_buttons(True, "Ready to flash!") + + def startflash(self): + """ + Start the actual flashing process + """ + if self.p: # don't continue if process already running + return + + self.openocd_log = "" + self.progress.setValue(0) + + source = self.filedir.toPlainText() + address, interface = read_config_file(self.status) + + self.searchbtn.setEnabled(False) + self.flashbtn.setEnabled(False) + self.confbtn.setEnabled(False) + + self.progress.setValue(10) + self.status.setText("Flashing...") + self.status.repaint() + + command = ( + 'openocd -f "interface/{}" ' + '-f "target/nrf52.cfg" -c "init" ' + '-c "program {} {} verify reset exit"' + ).format(interface, source, address) + + self.p = QProcess() # Keep a reference while it's running + self.p.finished.connect(self.flash_finished) # Clean up + self.p.readyReadStandardError.connect(self.handle_stderr) + self.p.start(command) + + def flash_finished(self): + """ + Handle flash completion + """ + if self.p.exitCode() == 0: + self.status.setText("Success!") + else: + self.status.setText("Something probably went wrong :(") + self.progress.setValue(0) + dlg = LogViewDialog(self.openocd_log) + dlg.exec() + + self.p = None + + self.searchbtn.setEnabled(True) + self.flashbtn.setEnabled(True) + self.confbtn.setEnabled(True) + + def handle_stderr(self): + """ + Capture output during flashing and process it + """ + data = self.p.readAllStandardError() + stderr = bytes(data).decode("utf8") + self.openocd_log = self.openocd_log + stderr + progress = progress_parser(stderr) + if progress: + self.progress.setValue(progress) + if progress == 70: + self.status.setText("Verifying...") + + def filesearch(self): + filedialog = QFileDialog() + datafile = filedialog.getOpenFileName( + caption="Select firmware file to flash...", + directory=str(Path.home() / "Downloads"), + filter="PineTime Firmware (*.bin *.hex)", + ) + + if datafile[0]: + self.filedir.setPlainText(datafile[0]) + self.progress.setValue(0) + + def confButton(self, s): + dlg = ConfDialog() + dlg.exec() + self.update_control_statuses() + + def info_button(self): + dlg = InfoDialog() + dlg.exec() + + +class ConfDialog(QDialog): + """ + Configuration class and UI + """ + + def __init__(self, parent=None): + super().__init__(parent=parent) + + self.firmware_types = [ + {"name": "mcuboot-app", "address": "0x00008000"}, + {"name": "bootloader", "address": "0x00000000"} + ] + + self.setWindowTitle("Flash Configuration") + self.resize(300, 220) + + self.addrinfo = QLabel("Firmware type (used to determine address):") + self.addrbox = QComboBox() + for firmware in self.firmware_types: + self.addrbox.addItem(firmware["name"], firmware["address"]) + + self.ifaceinfo = QLabel("Enter the interface (default: stlink.cfg)") + self.ifacebox = QPlainTextEdit() + + self.savebtn = QPushButton("Save configuration") + self.openocd_btn = QPushButton("Download OpenOCD") + self.status = QLabel("") + + conflayout = QVBoxLayout() + conflayout.addWidget(self.addrinfo) + conflayout.addWidget(self.addrbox) + conflayout.addWidget(self.ifaceinfo) + conflayout.addWidget(self.ifacebox) + conflayout.addWidget(self.savebtn) + conflayout.addWidget(self.openocd_btn) + conflayout.addWidget(self.status) + self.setLayout(conflayout) + + address, interface = read_config_file(self.status) + self.addrbox.setCurrentIndex(self.get_firmware_index(address)) + self.ifacebox.setPlainText(interface) + + self.savebtn.clicked.connect(self.saveconf) + self.openocd_btn.clicked.connect(self.setup_openocd) + + self.setWindowModality(Qt.ApplicationModal) + + def get_firmware_index(self, address: str): + for i, firmware in enumerate(self.firmware_types): + if firmware["address"] == address: + return i + return 0 + + def saveconf(self, s): + addr = self.addrbox.currentData() + iface = self.ifacebox.toPlainText() or "stlink.cfg" + + try: + with open("conf.dat", "wb+") as f: + pickle.dump((addr, iface), f) + self.status.setText("Configuration Saved.") + except OSError: + self.status.setText("Unable to write configuration file!") + + def setup_openocd(self): + """ + Download and unpack OpenOCD for the current platform, then add it to the system path. + """ + self.status.setText("Finding latest OpenOCD release...") + self.status.repaint() + + base_url = "https://api.github.com/repos" + response = requests.get(f"{base_url}/xpack-dev-tools/openocd-xpack/releases/latest") + + details = response.content.decode('utf-8') + content = json.loads(details) + + archive_file, hash_file = self.get_github_assets(content["assets"]) + if not archive_file or not hash_file: + return + + if not self.compare_hashes(archive_file, hash_file): + self.status.setText("Hashes do not match - corrupted download!") + return + + self.unpack_archive(archive_file) + self.status.setText("OpenOCD successfully downloaded.") + os.remove(archive_file) + os.remove(hash_file) + + def compare_hashes(self, archive_file, hash_file): + self.status.setText("Computing hashes OpenOCD...") + self.status.repaint() + with open(archive_file, "rb") as fd: + hasher = hashlib.sha256() + hasher.update(fd.read()) + computed_hash = hasher.hexdigest() + with open(hash_file, "r") as fd: + provided_hash = fd.read().split(" ")[0] # format: " " + return computed_hash == provided_hash + + def unpack_archive(self, archive): + """ + Unpack the archive and shift files so that the files can always be found + under 'openocd/' (i.e. without a version number, which is how they are + currently packed). + """ + self.status.setText("Unpacking OpenOCD...") + self.status.repaint() + tmpdir_name = "openocd_tmp" + shutil.unpack_archive(archive, extract_dir=tmpdir_name) + tmpdir_contents = os.listdir(tmpdir_name) + + if len(tmpdir_contents) == 1: + shutil.move(os.path.join(tmpdir_name, tmpdir_contents[0]), "openocd") + os.rmdir(tmpdir_name) + else: + shutil.move(tmpdir_name, "openocd") + + def get_github_assets(self, assets: List[str]): + """ + Determine which package needs to be downloaded for OS/architecture + and attempt to download it. + """ + plat = { + "Windows": "win32", + "Linux": "linux", + "MacOS": "darwin", + }.get(platform.system(), "") + arch = { + "AMD64": "x64", + "x86_64": "x64", + "i386": "ia32" + }.get(platform.machine(), "") + + if not plat or not arch: + self.status.setText("Unable to determine appropriate OpenOCD download.") + return + + download_urls = [f["browser_download_url"] for f in assets if plat in f["name"] and arch in f["name"]] + filenames = [f["name"] for f in assets if plat in f["name"] and arch in f["name"]] + assert len(download_urls) == 2 + + self.status.setText("Downloading OpenOCD from GitHub...") + self.status.repaint() + if not os.path.exists(filenames[0]) and not os.path.exists(filenames[1]): + wget.download(download_urls[0]) + wget.download(download_urls[1]) + + filenames.sort() + return filenames + + +class InfoDialog(QDialog): + """ + Info screen class and UI + """ + + def __init__(self, parent=None): + super().__init__(parent=parent) + + self.setWindowTitle("About PineTime Flasher v{}".format(__version__)) + self.resize(300, 200) + + vbox = QVBoxLayout() + textView = QLabel("""PineTime Flasher is a simple GUI software written in Python, +that uses the xpack-openOCD tool for flashing the PineTime +with SWD debuggers such as the ST-Link and J-Link. + +When first using the software, it is recommended that you +setup the configuration by choosing the appropriate firmware +type and flashing interface. + +The possible firmware types are: +* mcuboot-app +* bootloader + +For the interface, the options available are dependent on the +(*.cfg) provided by the xpack-openOCD itself. For example: +stlink.cfg or jlink.cfg""") + + vbox.addWidget(textView) + self.setLayout(vbox) + + self.setWindowModality(Qt.ApplicationModal) + + +class LogViewDialog(QDialog): + """ + Log view class and UI + """ + + def __init__(self, openocd_log, parent=None): + super().__init__(parent=parent) + + self.setWindowTitle("OpenOCD Flash Output") + self.resize(650, 300) + + vbox = QVBoxLayout() + infoText = QLabel("The flash operation encounter an error. Read the below log to find out why.") + logView = QPlainTextEdit(openocd_log) + + vbox.addWidget(infoText) + vbox.addWidget(logView) + self.setLayout(vbox) + + self.setWindowModality(Qt.ApplicationModal) + + +if __name__ == "__main__": + app = QApplication(sys.argv) + app.setStyle("Fusion") + + appDir = getattr(sys, "_MEIPASS", os.path.abspath(os.path.dirname(__file__))) + path_to_icon = os.path.abspath(os.path.join(appDir, "PinetimeFlasher.ico")) + + app_icon = QIcon(path_to_icon) + app.setWindowIcon(app_icon) + + qp = QPalette() + qp.setColor(QPalette.ButtonText, Qt.white) + qp.setColor(QPalette.Window, Qt.gray) + qp.setColor(QPalette.Button, Qt.gray) + app.setPalette(qp) + + add_openocd_to_system_path() + + win = ptflasher() + win.show() + sys.exit(app.exec_())