Mon, 22 Jul 2019 20:17:33 +0200
MicroPython: continued implementing the file manager widget.
--- a/eric6/MicroPython/MicroPythonFileManagerWidget.py Mon Jul 22 19:19:06 2019 +0200 +++ b/eric6/MicroPython/MicroPythonFileManagerWidget.py Mon Jul 22 20:17:33 2019 +0200 @@ -25,6 +25,7 @@ import UI.PixmapCache import Preferences +import Utilities class MicroPythonFileManagerWidget(QWidget, Ui_MicroPythonFileManagerWidget): @@ -45,6 +46,8 @@ self.putButton.setIcon(UI.PixmapCache.getIcon("1rightarrow")) self.getButton.setIcon(UI.PixmapCache.getIcon("1leftarrow")) + self.localUpButton.setIcon(UI.PixmapCache.getIcon("1uparrow")) + self.deviceUpButton.setIcon(UI.PixmapCache.getIcon("1uparrow")) self.putButton.setEnabled(False) self.getButton.setEnabled(False) @@ -161,11 +164,13 @@ if not dirname: dirname = os.getcwd() + if dirname.endswith(os.sep): + dirname = dirname[:-1] self.localCwd.setText(dirname) filesStatList = listdir_stat(dirname) filesList = [( - decoratedName(f, s[0]), + decoratedName(f, s[0], os.path.isdir(os.path.join(dirname, f))), mode2string(s[0]), str(s[6]), mtime2string(s[8])) for f, s in filesStatList] @@ -180,22 +185,43 @@ @pyqtSlot(QTreeWidgetItem, int) def on_localFileTreeWidget_itemActivated(self, item, column): """ - Slot documentation goes here. + Private slot to handle the activation of a local item. - @param item DESCRIPTION + If the item is a directory, the list will be re-populated for this + directory. + + @param item reference to the activated item @type QTreeWidgetItem - @param column DESCRIPTION + @param column column of the activation @type int """ - # TODO: not implemented yet - # show listing of activated directory + name = os.path.join(self.localCwd.text(), item.text(0)) + if name.endswith("/"): + # directory names end with a '/' + self.__listLocalFiles(name) + elif Utilities.MimeTypes.isTextFile(name): + e5App().getObject("ViewManager").getEditor(name) @pyqtSlot() def on_localFileTreeWidget_itemSelectionChanged(self): """ - Slot documentation goes here. + Private slot handling a change of selection in the local pane. """ - # TODO: not implemented yet + enable = bool(len(self.localFileTreeWidget.selectedItems())) + if enable: + enable &= not ( + self.localFileTreeWidget.selectedItems()[0].text(0) + .endswith("/")) + self.putButton.setEnabled(enable) + + @pyqtSlot() + def on_localUpButton_clicked(self): + """ + Private slot to go up one directory level. + """ + cwd = self.localCwd.text() + dirname = os.path.dirname(cwd) + self.__listLocalFiles(dirname) @pyqtSlot(QTreeWidgetItem, int) def on_deviceFileTreeWidget_itemActivated(self, item, column): @@ -218,6 +244,14 @@ # TODO: not implemented yet @pyqtSlot() + def on_deviceUpButton_clicked(self): + """ + Slot documentation goes here. + """ + # TODO: not implemented yet + raise NotImplementedError + + @pyqtSlot() def on_putButton_clicked(self): """ Slot documentation goes here.
--- a/eric6/MicroPython/MicroPythonFileManagerWidget.ui Mon Jul 22 19:19:06 2019 +0200 +++ b/eric6/MicroPython/MicroPythonFileManagerWidget.ui Mon Jul 22 20:17:33 2019 +0200 @@ -23,7 +23,7 @@ <property name="bottomMargin"> <number>2</number> </property> - <item row="0" column="0" colspan="2"> + <item row="0" column="0"> <widget class="QLabel" name="label"> <property name="text"> <string>Local Files</string> @@ -76,45 +76,6 @@ </column> </widget> </item> - <item row="1" column="2"> - <widget class="QTreeWidget" name="deviceFileTreeWidget"> - <property name="alternatingRowColors"> - <bool>true</bool> - </property> - <property name="rootIsDecorated"> - <bool>false</bool> - </property> - <property name="itemsExpandable"> - <bool>false</bool> - </property> - <property name="sortingEnabled"> - <bool>true</bool> - </property> - <attribute name="headerShowSortIndicator" stdset="0"> - <bool>true</bool> - </attribute> - <column> - <property name="text"> - <string>Name</string> - </property> - </column> - <column> - <property name="text"> - <string>Mode</string> - </property> - </column> - <column> - <property name="text"> - <string>Size</string> - </property> - </column> - <column> - <property name="text"> - <string>Time</string> - </property> - </column> - </widget> - </item> <item row="1" column="1"> <layout class="QVBoxLayout" name="verticalLayout"> <item> @@ -159,14 +120,97 @@ </item> </layout> </item> + <item row="1" column="2"> + <widget class="QTreeWidget" name="deviceFileTreeWidget"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="rootIsDecorated"> + <bool>false</bool> + </property> + <property name="itemsExpandable"> + <bool>false</bool> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + <attribute name="headerShowSortIndicator" stdset="0"> + <bool>true</bool> + </attribute> + <column> + <property name="text"> + <string>Name</string> + </property> + </column> + <column> + <property name="text"> + <string>Mode</string> + </property> + </column> + <column> + <property name="text"> + <string>Size</string> + </property> + </column> + <column> + <property name="text"> + <string>Time</string> + </property> + </column> + </widget> + </item> <item row="2" column="0"> - <widget class="QLabel" name="localCwd"/> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QLabel" name="localCwd"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="localUpButton"> + <property name="toolTip"> + <string>Press to move one directory level up</string> + </property> + </widget> + </item> + </layout> </item> <item row="2" column="2"> - <widget class="QLabel" name="deviceCwd"/> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QLabel" name="deviceCwd"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="deviceUpButton"> + <property name="toolTip"> + <string>Press to move one directory level up</string> + </property> + </widget> + </item> + </layout> </item> </layout> </widget> + <tabstops> + <tabstop>localFileTreeWidget</tabstop> + <tabstop>deviceFileTreeWidget</tabstop> + <tabstop>putButton</tabstop> + <tabstop>getButton</tabstop> + <tabstop>localUpButton</tabstop> + <tabstop>deviceUpButton</tabstop> + </tabstops> <resources/> <connections/> </ui>
--- a/eric6/MicroPython/MicroPythonFileSystem.py Mon Jul 22 19:19:06 2019 +0200 +++ b/eric6/MicroPython/MicroPythonFileSystem.py Mon Jul 22 20:17:33 2019 +0200 @@ -12,6 +12,7 @@ import ast import time import stat +import os from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject, QThread @@ -34,6 +35,7 @@ <li>rm: remove a file from the connected device</li> <li>mkdir: create a new directory</li> <li>rmdir: remove an empty directory</li> + <li>version: get version info about MicroPython</li> </ul> """ def __init__(self, parent=None): @@ -303,7 +305,7 @@ if err: raise IOError(self.__shortError(err)) - def put(self, hostFileName, deviceFileName): + def put(self, hostFileName, deviceFileName=None): """ Public method to copy a local file to the connected device. @@ -315,9 +317,31 @@ @rtype bool @exception IOError raised to indicate an issue with the device """ - # TODO: not implemented yet + if not os.path.isfile(hostFileName): + raise IOError("No such file: {0}".format(hostFileName)) + + with open(hostFileName, "rb") as hostFile: + content = hostFile.read() + + if not deviceFileName: + deviceFileName = os.path.basename(hostFileName) + + commands = [ + "fd = open('{0}', 'wb')".format(deviceFileName), + "f = fd.write", + ] + while content: + chunk = content[:64] + commands.append("f(" + repr(chunk) + ")") + content = content[64:] + commands.append("fd.close()") + + out, err = self.__execute(commands) + if err: + raise IOError(self.__shortError(err)) + return True - def get(self, deviceFileName, hostFileName): + def get(self, deviceFileName, hostFileName=None): """ Public method to copy a file from the connected device. @@ -329,7 +353,117 @@ @rtype bool @exception IOError raised to indicate an issue with the device """ - # TODO: not implemented yet + if not hostFileName: + hostFileName = deviceFileName + + commands = [ + "\n".join([ + "try:", + " from microbit import uart as u", + "except ImportError:", + " try:", + " from machine import UART", + " u = UART(0, {0})".format(115200), + " except Exception:", + " try:", + " from sys import stdout as u", + " except Exception:", + " raise Exception('Could not find UART module in" + " device.')", + ]), + "f = open('{0}', 'rb')".format(deviceFileName), + "r = f.read", + "result = True", + "\n".join([ + "while result:", + " result = r(32)" + " if result:", + " u.write(result)", + ]), + "f.close()", + ] + out, err = self.__execute(commands) + if err: + raise IOError(self.__shortError(err)) + + # write the received bytes to the local file + with open(hostFileName, "wb") as hostFile: + hostFile.write(out) + return True + + # TODO: add rsync function + + def version(self): + """ + Public method to get the MicroPython version information of the + connected device. + + @return dictionary containing the version information + @rtype dict + @exception ValueError raised to indicate that the device might not be + running MicroPython or there was an issue parsing the output + """ + commands = [ + "import os", + "print(os.uname())", + ] + try: + out, err = self.__execute(commands) + if err: + raise ValueError(self.__shortError(err)) + except ValueError: + # just re-raise it + raise + except Exception: + # Raise a value error to indicate being unable to find something + # on the device that will return parseable information about the + # version. It doesn't matter what the error is, it just needs to + # report a failure with the expected ValueError exception. + raise ValueError("Unable to determine version information.") + + rawOutput = out.decode("utf-8").strip() + rawOutput = rawOutput[1:-1] + items = rawOutput.split(",") + result = {} + for item in items: + key, value = item.strip().split("=") + result[key.strip()] = value.strip()[1:-1] + return result + + def syncTime(self): + """ + Public method to set the time of the connected device to the local + computer's time. + + @exception IOError raised to indicate an issue with the device + """ + now = time.localtime(time.time()) + commands = [ + "\n".join([ + "def set_time(rtc_time):", + " rtc = None", + " try:", # Pyboard (it doesn't have machine.RTC()) + " import pyb", + " rtc = pyb.RTC()", + " rtc.datetime(rtc_time)", + " except:", + " try:", + " import machine", + " rtc = machine.RTC()", + " try:", # ESP8266 uses rtc.datetime() + " rtc.datetime(rtc_time)", + " except:", # ESP32 uses rtc.init() + " rtc.init(rtc_time)", + " except:", + " pass", + ]), + "set_time({0})".format((now.tm_year, now.tm_mon, now.tm_mday, + now.tm_wday + 1, now.tm_hour, now.tm_min, + now.tm_sec, 0)) + ] + out, err = self.__execute(commands) + if err: + raise IOError(self.__shortError(err)) class MicroPythonFileManager(QObject): @@ -341,17 +475,35 @@ name, mode, size and time for each directory entry @signal currentDir(dirname) emitted to report the current directory of the device + @signal getFileDone(deviceFile, localFile) emitted after the file was + fetched from the connected device and written to the local file system + @signal putFileDone(localFile, deviceFile) emitted after the file was + copied to the connected device + @signal deleteFileDone(deviceFile) emitted after the file has been deleted + on the connected device @signal longListFilesFailed(exc) emitted with a failure message to indicate a failed long listing operation @signal currentDirFailed(exc) emitted with a failure message to indicate that the current directory is not available + @signal getFileFailed(exc) emitted with a failure message to indicate that + the file could not be fetched + @signal putFileFailed(exc) emitted with a failure message to indicate that + the file could not be copied + @signal deleteFileFailed(exc) emitted with a failure message to indicate + that the file could not be deleted on the device """ longListFiles = pyqtSignal(tuple) currentDir = pyqtSignal(str) + getFileDone = pyqtSignal(str, str) + putFileDone = pyqtSignal(str, str) + deleteFileDone = pyqtSignal(str) longListFilesFailed = pyqtSignal(str) currentDirFailed = pyqtSignal(str) + getFileFailed = pyqtSignal(str) + putFileFailed = pyqtSignal(str) + deleteFileFailed = pyqtSignal(str) def __init__(self, port, parent=None): """ @@ -386,7 +538,7 @@ @pyqtSlot(str) def lls(self, dirname): """ - Public method to get a long listing of the given directory. + Public slot to get a long listing of the given directory. @param dirname name of the directory to list @type str @@ -405,13 +557,65 @@ @pyqtSlot() def pwd(self): """ - Public method to get the current directory of the device. + Public slot to get the current directory of the device. """ try: pwd = self.__fs.pwd() self.currentDir.emit(pwd) except Exception as exc: self.currentDirFailed.emit(str(exc)) + + @pyqtSlot(str) + @pyqtSlot(str, str) + def get(self, deviceFileName, hostFileName=""): + """ + Public slot to get a file from the connected device. + + @param deviceFileName name of the file on the device + @type str + @param hostFileName name of the local file + @type str + """ + if hostFileName and os.path.isdir(hostFileName): + # only a local directory was given + hostFileName = os.path.join(hostFileName, + os.path.basename(deviceFileName)) + try: + self.__fs.get(deviceFileName, hostFileName) + self.getFileDone.emit(deviceFileName, hostFileName) + except Exception as exc: + self.getFileFailed.emit(str(exc)) + + @pyqtSlot(str) + @pyqtSlot(str, str) + def put(self, hostFileName, deviceFileName=""): + """ + Public slot to put a file onto the device. + + @param hostFileName name of the local file + @type str + @param deviceFileName name of the file on the connected device + @type str + """ + try: + self.__fs.put(hostFileName, deviceFileName) + self.putFileDone(hostFileName, deviceFileName) + except Exception as exc: + self.putFileFailed(str(exc)) + + @pyqtSlot(str) + def delete(self, deviceFileName): + """ + Public slot to delete a file on the device. + + @param deviceFileName name of the file on the connected device + @type str + """ + try: + self.__fs.rm(deviceFileName) + self.deleteFileDone.emit(deviceFileName) + except Exception as exc: + self.deleteFileFailed(str(exc)) ################################################################## ## Utility methods below @@ -442,7 +646,7 @@ return stat.filemode(mode) -def decoratedName(name, mode): +def decoratedName(name, mode, isDir=False): """ Function to decorate the given name according to the given mode. @@ -450,10 +654,12 @@ @type str @param mode mode value @type int + @param isDir flag indicating that name is a directory + @type bool @return decorated file or directory name @rtype str """ - if stat.S_ISDIR(mode): + if stat.S_ISDIR(mode) or isDir: # append a '/' for directories return name + "/" else: