diff --git a/skyflash.py b/skyflash.py index a079304..2a5cbee 100755 --- a/skyflash.py +++ b/skyflash.py @@ -6,6 +6,7 @@ import subprocess import webbrowser from urllib.request import Request, urlopen +import time # GUI imports from PyQt5.QtGui import QGuiApplication @@ -14,7 +15,7 @@ # environment vars # TODO, set final real URLS -skybianUrl = "http://127.0.0.1:8080/d.big" +skybianUrl = "http://192.168.200.1:8080/d.big" manualUrl = "http://github.com/simelo/skyflash" # utils class @@ -25,20 +26,81 @@ def __init__(self): def shortenPath(self, fullpath, ccount): # TODO OS dependent FS char fpath = fullpath.split("/") - spath = fpath[-1] - if len(spath) > ccount: - spath = ".../" + spath[-ccount:] - else: - spath = ".../" + spath + fpath.reverse() + spath = fpath[0] + count = len(spath) + + # cycle from back to start to fit on ccount + for item in fpath: + if item == fpath[0]: + # filename + spath = item + else: + # folders + # TODO OS dependant FS char + tspath = item + "/" + spath + if len(tspath) > ccount: + spath = ".../" + spath + break + else: + spath = tspath + # spath has the final shorted path return spath + def eta(self, secs): + # takes int seconds to complete a task + # return str like this + # < 10 seconds: a few seconds + # > 10 seconds & < 1 minute: 45 seconds + # > 1 minute & < 59 minutes: 45 minutes + # > 1 hour: 1 hour 23 minutes + + # minutes to decide + mins = int(secs / 60) + hours = int(mins / 60) + out = "" + + if mins < 1: + if secs < 10: + out = "a few secs" + else: + out = "{} secs".format(secs) + elif mins < 59: + out = "{} min".format(mins) + else: + if hours > 1: + out = "{} hours {} min".format(hours, int(mins % 60)) + else: + out = "{} hour {} min".format(hours, int(mins % 60)) + + return out + + def speed(self, speed): + # takes speeds in bytes per second + # return str like this + # < 1 kb/s: 256 b/s + # > 1 kb/s & < 1 Mb/s: 23 kb/s + # > 1 Mb/s: 2.1 Mb/s + + k = speed / 1000 + M = k / 1000 + out = "" + + if M > 1: + out = "{:0.1f} Mb/s".format(M) + elif k > 1: + out = "{:0.1f} kb/s".format(k) + else: + out = "{} b/s".format(int(speed)) + + return out # signals class, to be used on threads; for all major tasks class WorkerSignals(QObject): data = pyqtSignal(str) error = pyqtSignal(tuple) - progress = pyqtSignal(float) + progress = pyqtSignal(float, str) result = pyqtSignal(str) finished = pyqtSignal(str) @@ -77,7 +139,7 @@ def run(self): self.signals.result.emit(result) finally: # Done - self.signals.finished.emit() + self.signals.finished.emit("Done") # main object definition @@ -88,9 +150,21 @@ class skyFlash(QObject): setStatus = pyqtSignal(str, arguments=["msg"]) # download signals + # target is download label dData = pyqtSignal(str, arguments=["data"]) + # target is proogress bar dProg = pyqtSignal(float, arguments=["percent"]) - dDone = pyqtSignal(str, arguments=["result"]) + # target is hide download buttons + dDone = pyqtSignal() + # target is download button text: Download + dDown = pyqtSignal() + + # download flags + downloadActive = False + downloadOk = False + + # network signals + netConfig = pyqtSignal() # thread pool threadpool = QThreadPool() @@ -103,17 +177,31 @@ def __init__(self, parent=None): def downloadSkybianData(self, data): self.dData.emit(data) - def downloadSkybianProg(self, percent): - self.dProg.emit(percent) + def downloadSkybianProg(self, percent, data): + self.dProg.emit(int(percent)) + self.setStatus.emit(data) def downloadSkybianError(self, error): print("Error: " + error) + # result is the path to the local file def downloadSkybianFile(self, file): - self.skybianFile = file + if self.downloadOk: + self.skybianFile = file + # TODO adjust the size of the path + self.dData.emit("Skybian image is: " + utils.shortenPath(file, 32)) + self.setStatus.emit("Choose your network configuration") + else: + self.dData.emit("") + self.setStatus.emit("Download canceled or error happened") + self.dDown.emit() + # download finished, good or bad? def downloadSkybianDone(self, result): - self.dDone.emit(result) + # check status of download + if self.downloadOk: + self.dDone.emit() + self.netConfig.emit() # Download main trigger @pyqtSlot() @@ -121,6 +209,9 @@ def downloadSkybian(self): # check if there is a thread already working there downCount = self.threadpool.activeThreadCount() if downCount < 1: + # rise flag + self.downloadActive = True + # init download process self.down = Worker(self.skyDown) self.down.signals.data.connect(self.downloadSkybianData) @@ -131,10 +222,12 @@ def downloadSkybian(self): # init worker self.threadpool.start(self.down) else: - # TODO, emit status bar warning or modal box - print("There is a download in progress, please wait...") + # if you clicked it during a download, then you want to cancel + # just rise the flag an the thread will catch it and stop + self.downloadActive = False + self.downloadOk = False - # download skybian, will be instantiated in thread + # download skybian, will be instantiated in a thread def skyDown(self, data_callback, progress_callback): # take url for skybian from upper url = skybianUrl @@ -146,48 +239,70 @@ def skyDown(self, data_callback, progress_callback): fileName = url.split("/")[-1] # emit data of the download - data_callback.emit("There is {:04.1f}MB to download...".format(self.size/1000/1000)) + data_callback.emit("Downloading {:04.1f} MB".format(self.size/1000/1000)) # start download downloadedChunk = 0 - # chuck size @ 20kb - blockSize = 20480 - localFile = os.getcwd() + fileName + # chuck size @ 100kb + blockSize = 102400 + # TODO folder separator can be os depenndent, review + filePath = os.getcwd() + "/" + fileName + startTime = 0 + elapsedTime = 0 # DEBUG - print("Downloading to: {}".format(localFile)) - - with open(localFile, "wb") as finalImg: - while True: - chunk = req.read(blockSize) - if not chunk: - print("\nDownload Complete.") - break - downloadedChunk += len(chunk) - finalImg.write(chunk) - progress = float(downloadedChunk) / self.size - - # emit percent - progress_callback.emit(progress * 100) + print("Downloading to: {}".format(filePath)) - # final emit - print("Done") + try: + with open(filePath, "wb") as finalImg: + startTime = time.time() + while True: + chunk = req.read(blockSize) + if not chunk: + print("\nDownload Complete.") + break + downloadedChunk += len(chunk) + finalImg.write(chunk) + progress = (float(downloadedChunk) / self.size) * 100 + + # calc speed and ETA + elapsedTime = time.time() - startTime + bps = int(downloadedChunk/elapsedTime) # b/s + etas = int((self.size - downloadedChunk)/bps) # seconds + # emit progress + prog = "{:.1%}, {}, {} to go.".format(progress/100, utils.speed(bps), utils.eta(etas)) + progress_callback.emit(progress, prog) + + # check if the terminate flag is raised + if not self.downloadActive: + finalImg.close() + return "canceled" + + # close the file handle + finalImg.close() + self.downloadOk = True + + # return the local filename + return filePath - # close the file handle - finalImg.close() + except: + self.downloadOk = False + if finalImg: + finalImg.close() + os.unlink(finalImg) - # return the local filename - return localFile + return "Abnormal termination" # load skybian from a local file - @pyqtSlot() + @pyqtSlot(str) def localFile(self, file): if file is "": - self.setStatus.emit("You selected nothing, plase try again") + self.setStatus.emit("You selected nothing, please try again") return - if file.startswith("file://"): + # TODO, check on windows + if file.startswith("file:///"): file = file.replace("file://", "") print("Selected file is " + file) @@ -203,9 +318,10 @@ def localFile(self, file): self.setStatus.emit("Selected file is not readable.") return - # shorten the filename to fit on the label - filename = utils.shortenPath(file, 26) - self.downloadFinished.emit(filename) + # all seems good, emit ans go on + self.downloadOk = True + self.downloadSkybianFile(file) + self.downloadSkybianDone("Ok") # open the manual in the browser @pyqtSlot() diff --git a/skyflash.qml b/skyflash.qml index 79d6de8..d1784ac 100644 --- a/skyflash.qml +++ b/skyflash.qml @@ -96,26 +96,35 @@ ApplicationWindow { // buttons RowLayout { - // Download button - Button { - id: btDown - Layout.preferredHeight: 30 - Layout.preferredWidth: 80 - text: "Download" - tooltip: "Click here to download the base Skybian image from the official site" - onClicked: { skf.downloadSkybian() } - } + RowLayout { + id: phDownloadButtons + + // Download button + Button { + id: btDown + Layout.preferredHeight: 30 + Layout.preferredWidth: 80 + text: "Download" + tooltip: "Click here to download the base Skybian image from the official site" + + onClicked: { + skf.downloadSkybian() + btDown.text = "Cancel" + btDown.tooltip = "Click here to cancel the download" + } + } - // Browse button - Button { - id: btBrowse - Layout.preferredHeight: 30 - Layout.preferredWidth: 80 - text: "Browse" - tooltip: "Click here to browse a already downloaded Skybian image" + // Browse button + Button { + id: btBrowse + Layout.preferredHeight: 30 + Layout.preferredWidth: 80 + text: "Browse" + tooltip: "Click here to browse a already downloaded Skybian image" - onClicked: { fileDialog.open() } + onClicked: { fileDialog.open() } + } } // label @@ -133,7 +142,7 @@ ApplicationWindow { visible: true maximumValue: 100 minimumValue: 0 - value: 0.5 + value: 0.1 } } @@ -317,18 +326,28 @@ ApplicationWindow { // receiving the percent of the download onDProg: { pbDownload.value = percent - sbText.text = "Downloaded " + Number(percent).toLocaleString(Qt.locale("en_US")) + "% so far" } // download / local done onDDone: { - // inmediate actions - lbImageComment.text = result + // hide the buttons and progress bar. + pbDownload.visible = false + phDownloadButtons.visible = false + // just the label shows with the name/path to the file + // triggered by signal dResult + } + + // download canceled or in error, set back Download button + onDDown: { + btDown.text = "Download" + btDown.tooltip = "Click here to download the base Skybian image from the official site" + } + // start network config + onNetConfig: { // set next step visible boxNetwork.visible = true windows.height = 300 - } // status bar messages