MicroPython: continued implementing the file manager widget. micropython

Mon, 22 Jul 2019 20:17:33 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Mon, 22 Jul 2019 20:17:33 +0200
branch
micropython
changeset 7080
9a3adf033f90
parent 7079
d1564b590677
child 7081
ed510767c096

MicroPython: continued implementing the file manager widget.

eric6/MicroPython/MicroPythonFileManagerWidget.py file | annotate | diff | comparison | revisions
eric6/MicroPython/MicroPythonFileManagerWidget.ui file | annotate | diff | comparison | revisions
eric6/MicroPython/MicroPythonFileSystem.py file | annotate | diff | comparison | revisions
--- 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:

eric ide

mercurial