Refactored and improved the MicroPython code to be able to show the file manager and the REPL simultaneously. micropython

Mon, 29 Jul 2019 20:20:18 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Mon, 29 Jul 2019 20:20:18 +0200
branch
micropython
changeset 7095
8e10acb1cd85
parent 7094
d5f340dfb986
child 7097
f8c503462aac

Refactored and improved the MicroPython code to be able to show the file manager and the REPL simultaneously.

eric6.e4p file | annotate | diff | comparison | revisions
eric6/MicroPython/EspDevices.py file | annotate | diff | comparison | revisions
eric6/MicroPython/MicroPythonCommandsInterface.py file | annotate | diff | comparison | revisions
eric6/MicroPython/MicroPythonDevices.py file | annotate | diff | comparison | revisions
eric6/MicroPython/MicroPythonFileManager.py file | annotate | diff | comparison | revisions
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
eric6/MicroPython/MicroPythonReplWidget.py file | annotate | diff | comparison | revisions
eric6/MicroPython/MicroPythonReplWidget.ui file | annotate | diff | comparison | revisions
--- a/eric6.e4p	Sun Jul 28 18:55:00 2019 +0200
+++ b/eric6.e4p	Mon Jul 29 20:20:18 2019 +0200
@@ -457,9 +457,10 @@
     <Source>eric6/IconEditor/cursors/cursors_rc.py</Source>
     <Source>eric6/MicroPython/CircuitPythonDevices.py</Source>
     <Source>eric6/MicroPython/EspDevices.py</Source>
+    <Source>eric6/MicroPython/MicroPythonCommandsInterface.py</Source>
     <Source>eric6/MicroPython/MicroPythonDevices.py</Source>
+    <Source>eric6/MicroPython/MicroPythonFileManager.py</Source>
     <Source>eric6/MicroPython/MicroPythonFileManagerWidget.py</Source>
-    <Source>eric6/MicroPython/MicroPythonFileSystem.py</Source>
     <Source>eric6/MicroPython/MicroPythonFileSystemUtilities.py</Source>
     <Source>eric6/MicroPython/MicroPythonGraphWidget.py</Source>
     <Source>eric6/MicroPython/MicroPythonProgressInfoDialog.py</Source>
--- a/eric6/MicroPython/EspDevices.py	Sun Jul 28 18:55:00 2019 +0200
+++ b/eric6/MicroPython/EspDevices.py	Mon Jul 29 20:20:18 2019 +0200
@@ -61,13 +61,7 @@
             and a reason why it cannot.
         @rtype tuple of (bool, str)
         """
-        if self.__fileManagerActive:
-            return False, self.tr("The REPL and the file manager use the same"
-                                  " USB serial connection. Only one can be"
-                                  " active at any time. Toggle the file"
-                                  " manager off and try again.")
-        else:
-            return True, ""
+        return True, ""
     
     def setRepl(self, on):
         """
@@ -77,8 +71,6 @@
         @type bool
         """
         self.__replActive = on
-        self.microPython.setActionButtons(
-            files=not (on or self.__plotterActive))
     
     def canStartPlotter(self):
         """
@@ -88,13 +80,7 @@
             Plotter and a reason why it cannot.
         @rtype tuple of (bool, str)
         """
-        if self.__fileManagerActive:
-            return False, self.tr("The Plotter and the file manager use the"
-                                  " same USB serial connection. Only one can"
-                                  " be active at any time. Toggle the file"
-                                  " manager off and try again.")
-        else:
-            return True, ""
+        return True, ""
     
     def setPlotter(self, on):
         """
@@ -104,8 +90,6 @@
         @type bool
         """
         self.__plotterActive = on
-        self.microPython.setActionButtons(
-            files=not (on or self.__replActive))
     
     def canRunScript(self):
         """
@@ -135,13 +119,7 @@
             File Manager and a reason why it cannot.
         @rtype tuple of (bool, str)
         """
-        if self.__replActive or self.__plotterActive:
-            return False, self.tr("The file manager and the REPL/plotter use"
-                                  " the same USB serial connection. Only one"
-                                  " can be active at any time. Disconnect the"
-                                  " REPL/plotter and try again.")
-        else:
-            return True, ""
+        return True, ""
     
     def setFileManager(self, on):
         """
@@ -151,12 +129,10 @@
         @type bool
         """
         self.__fileManagerActive = on
-        self.microPython.setActionButtons(
-            run=not on, repl=not on, chart=HAS_QTCHART and not on)
     
     @pyqtSlot()
     def handleDataFlood(self):
         """
-        Public slot handling a data floof from the device.
+        Public slot handling a data flood from the device.
         """
         self.microPython.setActionButtons(files=True)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/MicroPython/MicroPythonCommandsInterface.py	Mon Jul 29 20:20:18 2019 +0200
@@ -0,0 +1,734 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2019 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing some file system commands for MicroPython.
+"""
+
+from __future__ import unicode_literals
+
+import ast
+import time
+import os
+
+from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject, QThread, QTimer
+
+from .MicroPythonSerialPort import MicroPythonSerialPort
+
+import Preferences
+
+
+class MicroPythonCommandsInterface(QObject):
+    """
+    Class implementing some file system commands for MicroPython.
+    
+    Commands are provided to perform operations on the file system of a
+    connected MicroPython device. Supported commands are:
+    <ul>
+    <li>ls: directory listing</li>
+    <li>lls: directory listing with meta data</li>
+    <li>cd: change directory</li>
+    <li>pwd: get the current directory</li>
+    <li>put: copy a file to the connected device</li>
+    <li>get: get a file from the connected device</li>
+    <li>rm: remove a file from the connected device</li>
+    <li>rmrf: remove a file/directory recursively (like 'rm -rf' in bash)
+    <li>mkdir: create a new directory</li>
+    <li>rmdir: remove an empty directory</li>
+    </ul>
+    
+    There are additional commands related to time and version.
+    <ul>
+    <li>version: get version info about MicroPython</li>
+    <li>getImplementation: get some implementation information</li>
+    <li>syncTime: synchronize the time of the connected device</li>
+    <li>showTime: show the current time of the connected device</li>
+    </ul>
+    
+    @signal executeAsyncFinished() emitted to indicate the end of an
+        asynchronously executed list of commands (e.g. a script)
+    @signal dataReceived(data) emitted to send data received via the serial
+        connection for further processing
+    """
+    executeAsyncFinished = pyqtSignal()
+    dataReceived = pyqtSignal(bytes)
+    
+    def __init__(self, parent=None):
+        """
+        Constructor
+        
+        @param parent reference to the parent object
+        @type QObject
+        """
+        super(MicroPythonCommandsInterface, self).__init__(parent)
+        
+        self.__blockReadyRead = False
+        
+        self.__serial = MicroPythonSerialPort(
+            timeout=Preferences.getMicroPython("SerialTimeout"),
+            parent=self)
+        self.__serial.readyRead.connect(self.__readSerial)
+    
+    @pyqtSlot()
+    def __readSerial(self):
+        """
+        Private slot to read all available serial data and emit it with the
+        "dataReceived" signal for further processing.
+        """
+        if not self.__blockReadyRead:
+            data = bytes(self.__serial.readAll())
+            self.dataReceived.emit(data)
+    
+    @pyqtSlot()
+    def connectToDevice(self, port):
+        """
+        Public slot to start the manager.
+        
+        @param port name of the port to be used
+        @type str
+        @return flag indicating success
+        @rtype bool
+        """
+        return self.__serial.openSerialLink(port)
+    
+    @pyqtSlot()
+    def disconnectFromDevice(self):
+        """
+        Public slot to stop the thread.
+        """
+        self.__serial.closeSerialLink()
+    
+    def isConnected(self):
+        """
+        Public method to get the connection status.
+        
+        @return flag indicating the connection status
+        @rtype bool
+        """
+        return self.__serial.isConnected()
+    
+    @pyqtSlot()
+    def handlePreferencesChanged(self):
+        """
+        Public slot to handle a change of the preferences.
+        """
+        self.__serial.setTimeout(Preferences.getMicroPython("SerialTimeout"))
+    
+    def write(self, data):
+        """
+        Public method to write data to the connected device.
+        
+        @param data data to be written
+        @type bytes or bytearray
+        """
+        self.__serial.isConnected() and self.__serial.write(data)
+    
+    def __rawOn(self):
+        """
+        Private method to switch the connected device to 'raw' mode.
+        
+        Note: switching to raw mode is done with synchronous writes.
+        
+        @return flag indicating success
+        @@rtype bool
+        """
+        if not self.__serial:
+            return False
+        
+        rawReplMessage = b"raw REPL; CTRL-B to exit\r\n"
+        softRebootMessage = b"soft reboot\r\n"
+        
+        self.__serial.write(b"\x02")        # end raw mode if required
+        self.__serial.waitForBytesWritten()
+        for _i in range(3):
+            # CTRL-C three times to break out of loops
+            self.__serial.write(b"\r\x03")
+            self.__serial.waitForBytesWritten()
+            QThread.msleep(10)
+        self.__serial.readAll()             # read all data and discard it
+        self.__serial.write(b"\r\x01")      # send CTRL-A to enter raw mode
+        self.__serial.readUntil(rawReplMessage)
+        if self.__serial.hasTimedOut():
+            return False
+        self.__serial.write(b"\x04")        # send CTRL-D to soft reset
+        self.__serial.readUntil(softRebootMessage)
+        if self.__serial.hasTimedOut():
+            return False
+        
+        # some MicroPython devices seem to need to be convinced in some
+        # special way
+        data = self.__serial.readUntil(rawReplMessage)
+        if self.__serial.hasTimedOut():
+            return False
+        if not data.endswith(rawReplMessage):
+            self.__serial.write(b"\r\x01")  # send CTRL-A again
+            self.__serial.readUntil(rawReplMessage)
+            if self.__serial.hasTimedOut():
+                return False
+        self.__serial.readAll()             # read all data and discard it
+        return True
+    
+    def __rawOff(self):
+        """
+        Private method to switch 'raw' mode off.
+        """
+        if self.__serial:
+            self.__serial.write(b"\x02")    # send CTRL-B to cancel raw mode
+    
+    def execute(self, commands):
+        """
+        Public method to send commands to the connected device and return the
+        result.
+        
+        If no serial connection is available, empty results will be returned.
+        
+        @param commands list of commands to be executed
+        @type str
+        @return tuple containing stdout and stderr output of the device
+        @rtype tuple of (bytes, bytes)
+        """
+        if not self.__serial:
+            return b"", b""
+        
+        if not self.__serial.isConnected():
+            return b"", b"Device not connected or not switched on."
+        
+        result = bytearray()
+        err = b""
+        
+        self.__blockReadyRead = True
+        ok = self.__rawOn()
+        if not ok:
+            self.__blockReadyRead = False
+            return (
+                b"",
+                b"Could not switch to raw mode. Is the device switched on?"
+            )
+        
+        QThread.msleep(10)
+        for command in commands:
+            if command:
+                commandBytes = command.encode("utf-8")
+                self.__serial.write(commandBytes + b"\x04")
+                # read until prompt
+                response = self.__serial.readUntil(b"\x04>")
+                if self.__serial.hasTimedOut():
+                    self.__blockReadyRead = False
+                    return b"", b"Timeout while processing commands."
+                if b"\x04" in response[2:-2]:
+                    # split stdout, stderr
+                    out, err = response[2:-2].split(b"\x04")
+                    result += out
+                else:
+                    err = b"invalid response received: " + response
+                if err:
+                    self.__blockReadyRead = False
+                    return b"", err
+        QThread.msleep(10)
+        self.__rawOff()
+        self.__blockReadyRead = False
+        
+        return bytes(result), err
+    
+    def executeAsync(self, commandsList):
+        """
+        Public method to execute a series of commands over a period of time
+        without returning any result (asynchronous execution).
+        
+        @param commandsList list of commands to be execute on the device
+        @type list of bytes
+        """
+        def remainingTask(commands):
+            self.executeAsync(commands)
+        
+        if commandsList:
+            command = commandsList[0]
+            self.__serial.write(command)
+            remainder = commandsList[1:]
+            QTimer.singleShot(2, lambda: remainingTask(remainder))
+        else:
+            self.executeAsyncFinished.emit()
+    
+    def __shortError(self, error):
+        """
+        Private method to create a shortened error message.
+        
+        @param error verbose error message
+        @type bytes
+        @return shortened error message
+        @rtype str
+        """
+        if error:
+            decodedError = error.decode("utf-8")
+            try:
+                return decodedError.split["\r\n"][-2]
+            except Exception:
+                return decodedError
+        return self.tr("Detected an error without indications.")
+    
+    ##################################################################
+    ## Methods below implement the file system commands
+    ##################################################################
+    
+    def ls(self, dirname=""):
+        """
+        Public method to get a directory listing of the connected device.
+        
+        @param dirname name of the directory to be listed
+        @type str
+        @return tuple containg the directory listing
+        @rtype tuple of str
+        @exception IOError raised to indicate an issue with the device
+        """
+        commands = [
+            "import os",
+            "print(os.listdir('{0}'))".format(dirname),
+        ]
+        out, err = self.execute(commands)
+        if err:
+            raise IOError(self.__shortError(err))
+        return ast.literal_eval(out.decode("utf-8"))
+    
+    def lls(self, dirname="", fullstat=False):
+        """
+        Public method to get a long directory listing of the connected device
+        including meta data.
+        
+        @param dirname name of the directory to be listed
+        @type str
+        @param fullstat flag indicating to return the full stat() tuple
+        @type bool
+        @return list containing the directory listing with tuple entries of
+            the name and and a tuple of mode, size and time (if fullstat is
+            false) or the complete stat() tuple. 'None' is returned in case the
+            directory doesn't exist.
+        @rtype tuple of (str, tuple)
+        @exception IOError raised to indicate an issue with the device
+        """
+        commands = [
+            "import os",
+            "\n".join([
+                "def stat(filename):",
+                "    try:",
+                "        rstat = os.lstat(filename)",
+                "    except:",
+                "        rstat = os.stat(filename)",
+                "    return tuple(rstat)",
+            ]),
+            "\n".join([
+                "def listdir_stat(dirname):",
+                "    try:",
+                "        files = os.listdir(dirname)",
+                "    except OSError:",
+                "        return None",
+                "    if dirname in ('', '/'):",
+                "        return list((f, stat(f)) for f in files)",
+                "    return list((f, stat(dirname + '/' + f)) for f in files)",
+            ]),
+            "print(listdir_stat('{0}'))".format(dirname),
+        ]
+        out, err = self.execute(commands)
+        if err:
+            raise IOError(self.__shortError(err))
+        fileslist = ast.literal_eval(out.decode("utf-8"))
+        if fileslist is None:
+            return None
+        else:
+            if fullstat:
+                return fileslist
+            else:
+                return [(f, (s[0], s[6], s[8])) for f, s in fileslist]
+    
+    def cd(self, dirname):
+        """
+        Public method to change the current directory on the connected device.
+        
+        @param dirname directory to change to
+        @type str
+        @exception IOError raised to indicate an issue with the device
+        """
+        assert dirname
+        
+        commands = [
+            "import os",
+            "os.chdir('{0}')".format(dirname),
+        ]
+        out, err = self.execute(commands)
+        if err:
+            raise IOError(self.__shortError(err))
+    
+    def pwd(self):
+        """
+        Public method to get the current directory of the connected device.
+        
+        @return current directory
+        @rtype str
+        @exception IOError raised to indicate an issue with the device
+        """
+        commands = [
+            "import os",
+            "print(os.getcwd())",
+        ]
+        out, err = self.execute(commands)
+        if err:
+            raise IOError(self.__shortError(err))
+        return out.decode("utf-8").strip()
+     
+    def rm(self, filename):
+        """
+        Public method to remove a file from the connected device.
+        
+        @param filename name of the file to be removed
+        @type str
+        @exception IOError raised to indicate an issue with the device
+        """
+        assert filename
+        
+        commands = [
+            "import os",
+            "os.remove('{0}')".format(filename),
+        ]
+        out, err = self.execute(commands)
+        if err:
+            raise IOError(self.__shortError(err))
+    
+    def rmrf(self, name, recursive=False, force=False):
+        """
+        Public method to remove a file or directory recursively.
+        
+        @param name of the file or directory to remove
+        @type str
+        @param recursive flag indicating a recursive deletion
+        @type bool
+        @param force flag indicating to ignore errors
+        @type bool
+        @return flag indicating success
+        @rtype bool
+        @exception IOError raised to indicate an issue with the device
+        """
+        assert name
+        
+        commands = [
+            "import os",
+            "\n".join([
+                "def remove_file(name, recursive=False, force=False):",
+                "    try:",
+                "        mode = os.stat(name)[0]",
+                "        if mode & 0x4000 != 0:",
+                "            if recursive:",
+                "                for file in os.listdir(name):",
+                "                    success = remove_file(name + '/' + file,"
+                " recursive, force)",
+                "                    if not success and not force:",
+                "                        return False",
+                "                os.rmdir(name)",
+                "            else:",
+                "                if not force:",
+                "                    return False",
+                "        else:",
+                "            os.remove(name)",
+                "    except:",
+                "        if not force:",
+                "            return False",
+                "    return True",
+            ]),
+            "print(remove_file('{0}', {1}, {2}))".format(name, recursive,
+                                                         force),
+        ]
+        out, err = self.execute(commands)
+        if err:
+            raise IOError(self.__shortError(err))
+        return ast.literal_eval(out.decode("utf-8"))
+    
+    def mkdir(self, dirname):
+        """
+        Public method to create a new directory.
+        
+        @param dirname name of the directory to create
+        @type str
+        @exception IOError raised to indicate an issue with the device
+        """
+        assert dirname
+   
+        commands = [
+            "import os",
+            "os.mkdir('{0}')".format(dirname),
+        ]
+        out, err = self.execute(commands)
+        if err:
+            raise IOError(self.__shortError(err))
+    
+    def rmdir(self, dirname):
+        """
+        Public method to remove a directory.
+        
+        @param dirname name of the directory to be removed
+        @type str
+        @exception IOError raised to indicate an issue with the device
+        """
+        assert dirname
+   
+        commands = [
+            "import os",
+            "os.rmdir('{0}')".format(dirname),
+        ]
+        out, err = self.execute(commands)
+        if err:
+            raise IOError(self.__shortError(err))
+    
+    def put(self, hostFileName, deviceFileName=None):
+        """
+        Public method to copy a local file to the connected device.
+        
+        @param hostFileName name of the file to be copied
+        @type str
+        @param deviceFileName name of the file to copy to
+        @type str
+        @return flag indicating success
+        @rtype bool
+        @exception IOError raised to indicate an issue with the device
+        """
+        if not os.path.isfile(hostFileName):
+            raise IOError("No such file: {0}".format(hostFileName))
+        
+        with open(hostFileName, "rb") as hostFile:
+            content = hostFile.read()
+            # convert eol '\r'
+            content = content.replace(b"\r\n", b"\r")
+            content = content.replace(b"\n", b"\r")
+        
+        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=None):
+        """
+        Public method to copy a file from the connected device.
+        
+        @param deviceFileName name of the file to copy
+        @type str
+        @param hostFileName name of the file to copy to
+        @type str
+        @return flag indicating success
+        @rtype bool
+        @exception IOError raised to indicate an issue with the device
+        """
+        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
+        # convert eol to "\n"
+        out = out.replace(b"\r\n", b"\n")
+        out = out.replace(b"\r", b"\n")
+        with open(hostFileName, "wb") as hostFile:
+            hostFile.write(out)
+        return True
+    
+    def fileSystemInfo(self):
+        """
+        Public method to obtain information about the currently mounted file
+        systems.
+        
+        @return tuple of tuples containing the file system name, the total
+            size, the used size and the free size
+        @rtype tuple of tuples of (str, int, int, int)
+        @exception IOError raised to indicate an issue with the device
+        """
+        commands = [
+            "import os",
+            "\n".join([
+                "def fsinfo():",
+                "    infolist = []",
+                "    fsnames = os.listdir('/')",
+                "    for fs in fsnames:",
+                "        fs = '/' + fs",
+                "        infolist.append((fs, os.statvfs(fs)))",
+                "    return infolist",
+            ]),
+            "print(fsinfo())",
+        ]
+        out, err = self.execute(commands)
+        if err:
+            raise IOError(self.__shortError(err))
+        infolist = ast.literal_eval(out.decode("utf-8"))
+        if infolist is None:
+            return None
+        else:
+            filesystemInfos = []
+            for fs, info in infolist:
+                totalSize = info[2] * info[1]
+                freeSize = info[4] * info[1]
+                usedSize = totalSize - freeSize
+                filesystemInfos.append((fs, totalSize, usedSize, freeSize))
+        
+        return tuple(filesystemInfos)
+    
+    ##################################################################
+    ## non-filesystem related methods below
+    ##################################################################
+    
+    def version(self):
+        """
+        Public method to get the MicroPython version information of the
+        connected device.
+        
+        @return dictionary containing the version information
+        @rtype dict
+        @exception IOError raised to indicate an issue with the device
+        """
+        commands = [
+            "import os",
+            "print(os.uname())",
+        ]
+        out, err = self.execute(commands)
+        if err:
+            raise IOError(self.__shortError(err))
+        
+        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 getImplementation(self):
+        """
+        Public method to get some implementation information of the connected
+        device.
+        
+        @return dictionary containing the implementation information
+        @rtype dict
+        @exception IOError raised to indicate an issue with the device
+        """
+        commands = [
+            "import sys",
+            "res = {}",                             # __IGNORE_WARNING_M613__
+            "\n".join([
+                "try:",
+                "    res['name'] = sys.implementation.name",
+                "except AttributeError:",
+                "    res['name'] = 'unknown'",
+            ]),
+            "\n".join([
+                "try:",
+                "    res['version'] = '.'.join((str(i) for i in"
+                " sys.implementation.version))",
+                "except AttributeError:",
+                "    res['version'] = 'unknown'",
+            ]),
+            "print(res)",
+        ]
+        out, err = self.execute(commands)
+        if err:
+            raise IOError(self.__shortError(err))
+        return ast.literal_eval(out.decode("utf-8"))
+    
+    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()",
+                "        clock_time = rtc_time[:6] + (rtc_time[6] + 1, 0)",
+                "        rtc.datetime(clock_time)",
+                "    except:",
+                "        try:",
+                "            import machine",
+                "            rtc = machine.RTC()",
+                "            try:",     # ESP8266 may use rtc.datetime()
+                "                clock_time = rtc_time[:6] +"
+                " (rtc_time[6] + 1, 0)",
+                "                rtc.datetime(clock_time)",
+                "            except:",  # ESP32 uses rtc.init()
+                "                rtc.init(rtc_time[:6])",
+                "        except:",
+                "            try:",
+                "                import rtc, time",
+                "                clock=rtc.RTC()",
+                "                clock.datetime = time.struct_time(rtc_time +"
+                " (-1, -1))",
+                "            except:",
+                "                pass",
+            ]),
+            "set_time({0})".format((now.tm_year, now.tm_mon, now.tm_mday,
+                                    now.tm_hour, now.tm_min, now.tm_sec,
+                                    now.tm_wday))
+        ]
+        out, err = self.execute(commands)
+        if err:
+            raise IOError(self.__shortError(err))
+    
+    def showTime(self):
+        """
+        Public method to get the current time of the device.
+        
+        @return time of the device
+        @rtype str
+        @exception IOError raised to indicate an issue with the device
+        """
+        commands = [
+            "import time",
+            "print(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()))",
+            # __IGNORE_WARNING_M601__
+        ]
+        out, err = self.execute(commands)
+        if err:
+            raise IOError(self.__shortError(err))
+        return out.decode("utf-8").strip()
--- a/eric6/MicroPython/MicroPythonDevices.py	Sun Jul 28 18:55:00 2019 +0200
+++ b/eric6/MicroPython/MicroPythonDevices.py	Mon Jul 29 20:20:18 2019 +0200
@@ -291,7 +291,7 @@
         commands.append(b'\x04')
         rawOff = [b'\x02']
         commandSequence = rawOn + newLine + commands + rawOff
-        self.microPython.execute(commandSequence)
+        self.microPython.commandsInterface().executeAsync(commandSequence)
     
     @pyqtSlot()
     def handleDataFlood(self):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/MicroPython/MicroPythonFileManager.py	Mon Jul 29 20:20:18 2019 +0200
@@ -0,0 +1,446 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2019 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing some file system commands for MicroPython.
+"""
+
+from __future__ import unicode_literals
+
+import os
+import stat
+
+from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject
+
+from .MicroPythonFileSystemUtilities import (
+    mtime2string, mode2string, decoratedName, listdirStat
+)
+
+
+# TODO: modify to use MicroPythonCommandsInterface
+class MicroPythonFileManager(QObject):
+    """
+    Class implementing an interface to the device file system commands with
+    some additional sugar.
+    
+    @signal longListFiles(result) emitted with a tuple of tuples containing the
+        name, mode, size and time for each directory entry
+    @signal currentDir(dirname) emitted to report the current directory of the
+        device
+    @signal currentDirChanged(dirname) emitted to report back a change of the
+        current directory
+    @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 rsyncDone(localName, deviceName) emitted after the rsync operation
+        has been completed
+    @signal rsyncProgressMessage(msg) emitted to send a message about what
+        rsync is doing
+    @signal removeDirectoryDone() emitted after a directory has been deleted
+    @signal createDirectoryDone() emitted after a directory was created
+    @signal fsinfoDone(fsinfo) emitted after the file system information was
+        obtained
+    
+    @signal synchTimeDone() emitted after the time was synchronizde to the
+        device
+    @signal showTimeDone(dateTime) emitted after the date and time was fetched
+        from the connected device
+    @signal showVersionDone(versionInfo) emitted after the version information
+        was fetched from the connected device
+    @signal showImplementationDone(name,version) emitted after the
+        implementation information has been obtained
+    
+    @signal error(exc) emitted with a failure message to indicate a failure
+        during the most recent operation
+    """
+    longListFiles = pyqtSignal(tuple)
+    currentDir = pyqtSignal(str)
+    currentDirChanged = pyqtSignal(str)
+    getFileDone = pyqtSignal(str, str)
+    putFileDone = pyqtSignal(str, str)
+    deleteFileDone = pyqtSignal(str)
+    rsyncDone = pyqtSignal(str, str)
+    rsyncProgressMessage = pyqtSignal(str)
+    removeDirectoryDone = pyqtSignal()
+    createDirectoryDone = pyqtSignal()
+    fsinfoDone = pyqtSignal(tuple)
+    
+    synchTimeDone = pyqtSignal()
+    showTimeDone = pyqtSignal(str)
+    showVersionDone = pyqtSignal(dict)
+    showImplementationDone = pyqtSignal(str, str)
+    
+    error = pyqtSignal(str, str)
+    
+    def __init__(self, commandsInterface, parent=None):
+        """
+        Constructor
+        
+        @param commandsInterface reference to the commands interface object
+        @type MicroPythonCommandsInterface
+        @param parent reference to the parent object
+        @type QObject
+        """
+        super(MicroPythonFileManager, self).__init__(parent)
+        
+        self.__commandsInterface = commandsInterface
+    
+    @pyqtSlot(str)
+    def lls(self, dirname):
+        """
+        Public slot to get a long listing of the given directory.
+        
+        @param dirname name of the directory to list
+        @type str
+        """
+        try:
+            filesList = self.__commandsInterface.lls(dirname)
+            result = [(decoratedName(name, mode),
+                       mode2string(mode),
+                       str(size),
+                       mtime2string(mtime)) for
+                      name, (mode, size, mtime) in filesList]
+            self.longListFiles.emit(tuple(result))
+        except Exception as exc:
+            self.error.emit("lls", str(exc))
+    
+    @pyqtSlot()
+    def pwd(self):
+        """
+        Public slot to get the current directory of the device.
+        """
+        try:
+            pwd = self.__commandsInterface.pwd()
+            self.currentDir.emit(pwd)
+        except Exception as exc:
+            self.error.emit("pwd", str(exc))
+    
+    @pyqtSlot(str)
+    def cd(self, dirname):
+        """
+        Public slot to change the current directory of the device.
+        
+        @param dirname name of the desired current directory
+        @type str
+        """
+        try:
+            self.__commandsInterface.cd(dirname)
+            self.currentDirChanged.emit(dirname)
+        except Exception as exc:
+            self.error.emit("cd", 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.__commandsInterface.get(deviceFileName, hostFileName)
+            self.getFileDone.emit(deviceFileName, hostFileName)
+        except Exception as exc:
+            self.error.emit("get", 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.__commandsInterface.put(hostFileName, deviceFileName)
+            self.putFileDone.emit(hostFileName, deviceFileName)
+        except Exception as exc:
+            self.error.emit("put", 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.__commandsInterface.rm(deviceFileName)
+            self.deleteFileDone.emit(deviceFileName)
+        except Exception as exc:
+            self.error.emit("delete", str(exc))
+    
+    def __rsync(self, hostDirectory, deviceDirectory, mirror=True):
+        """
+        Private method to synchronize a local directory to the device.
+        
+        @param hostDirectory name of the local directory
+        @type str
+        @param deviceDirectory name of the directory on the device
+        @type str
+        @param mirror flag indicating to mirror the local directory to
+            the device directory
+        @type bool
+        @return list of errors
+        @rtype list of str
+        """
+        errors = []
+        
+        if not os.path.isdir(hostDirectory):
+            return [self.tr(
+                "The given name '{0}' is not a directory or does not exist.")
+                .format(hostDirectory)
+            ]
+        
+        self.rsyncProgressMessage.emit(
+            self.tr("Synchronizing <b>{0}</b>.").format(deviceDirectory)
+        )
+        
+        sourceDict = {}
+        sourceFiles = listdirStat(hostDirectory)
+        for name, nstat in sourceFiles:
+            sourceDict[name] = nstat
+        
+        destinationDict = {}
+        try:
+            destinationFiles = self.__commandsInterface.lls(deviceDirectory,
+                                                            fullstat=True)
+        except Exception as exc:
+            return [str(exc)]
+        if destinationFiles is None:
+            # the destination directory does not exist
+            try:
+                self.__commandsInterface.mkdir(deviceDirectory)
+            except Exception as exc:
+                return [str(exc)]
+        else:
+            for name, nstat in destinationFiles:
+                destinationDict[name] = nstat
+        
+        destinationSet = set(destinationDict.keys())
+        sourceSet = set(sourceDict.keys())
+        toAdd = sourceSet - destinationSet                  # add to dev
+        toDelete = destinationSet - sourceSet               # delete from dev
+        toUpdate = destinationSet.intersection(sourceSet)   # update files
+        
+        for sourceBasename in toAdd:
+            # name exists in source but not in device
+            sourceFilename = os.path.join(hostDirectory, sourceBasename)
+            destFilename = deviceDirectory + "/" + sourceBasename
+            self.rsyncProgressMessage.emit(
+                self.tr("Adding <b>{0}</b>...").format(destFilename))
+            if os.path.isfile(sourceFilename):
+                try:
+                    self.__commandsInterface.put(sourceFilename, destFilename)
+                except Exception as exc:
+                    # just note issues but ignore them otherwise
+                    errors.append(str(exc))
+            if os.path.isdir(sourceFilename):
+                # recurse
+                errs = self.__rsync(sourceFilename, destFilename,
+                                    mirror=mirror)
+                # just note issues but ignore them otherwise
+                errors.extend(errs)
+        
+        if mirror:
+            for destBasename in toDelete:
+                # name exists in device but not local, delete
+                destFilename = deviceDirectory + "/" + destBasename
+                self.rsyncProgressMessage.emit(
+                    self.tr("Removing <b>{0}</b>...").format(destFilename))
+                try:
+                    self.__commandsInterface.rmrf(destFilename, recursive=True,
+                                                  force=True)
+                except Exception as exc:
+                    # just note issues but ignore them otherwise
+                    errors.append(str(exc))
+        
+        for sourceBasename in toUpdate:
+            # names exist in both; do an update
+            sourceStat = sourceDict[sourceBasename]
+            destStat = destinationDict[sourceBasename]
+            sourceFilename = os.path.join(hostDirectory, sourceBasename)
+            destFilename = deviceDirectory + "/" + sourceBasename
+            destMode = destStat[0]
+            if os.path.isdir(sourceFilename):
+                if stat.S_ISDIR(destMode):
+                    # both are directories => recurs
+                    errs = self.__rsync(sourceFilename, destFilename,
+                                        mirror=mirror)
+                    # just note issues but ignore them otherwise
+                    errors.extend(errs)
+                else:
+                    self.rsyncProgressMessage.emit(
+                        self.tr("Source <b>{0}</b> is a directory and"
+                                " destination <b>{1}</b> is a file. Ignoring"
+                                " it.")
+                        .format(sourceFilename, destFilename)
+                    )
+            else:
+                if stat.S_ISDIR(destMode):
+                    self.rsyncProgressMessage.emit(
+                        self.tr("Source <b>{0}</b> is a file and destination"
+                                " <b>{1}</b> is a directory. Ignoring it.")
+                        .format(sourceFilename, destFilename)
+                    )
+                else:
+                    if sourceStat[8] > destStat[8]:     # mtime
+                        self.rsyncProgressMessage.emit(
+                            self.tr("Updating <b>{0}</b>...")
+                            .format(destFilename)
+                        )
+                        try:
+                            self.__commandsInterface.put(sourceFilename,
+                                                         destFilename)
+                        except Exception as exc:
+                            errors.append(str(exc))
+        
+        self.rsyncProgressMessage.emit(
+            self.tr("Done synchronizing <b>{0}</b>.").format(deviceDirectory)
+        )
+        
+        return errors
+    
+    @pyqtSlot(str, str)
+    @pyqtSlot(str, str, bool)
+    def rsync(self, hostDirectory, deviceDirectory, mirror=True):
+        """
+        Public slot to synchronize a local directory to the device.
+        
+        @param hostDirectory name of the local directory
+        @type str
+        @param deviceDirectory name of the directory on the device
+        @type str
+        @param mirror flag indicating to mirror the local directory to
+            the device directory
+        @type bool
+        """
+        errors = self.__rsync(hostDirectory, deviceDirectory, mirror=mirror)
+        if errors:
+            self.error.emit("rsync", "\n".join(errors))
+        
+        self.rsyncDone.emit(hostDirectory, deviceDirectory)
+    
+    @pyqtSlot(str)
+    def mkdir(self, dirname):
+        """
+        Public slot to create a new directory.
+        
+        @param dirname name of the directory to create
+        @type str
+        """
+        try:
+            self.__commandsInterface.mkdir(dirname)
+            self.createDirectoryDone.emit()
+        except Exception as exc:
+            self.error.emit("mkdir", str(exc))
+    
+    @pyqtSlot(str)
+    @pyqtSlot(str, bool)
+    def rmdir(self, dirname, recursive=False):
+        """
+        Public slot to (recursively) remove a directory.
+        
+        @param dirname name of the directory to be removed
+        @type str
+        @param recursive flag indicating a recursive removal
+        @type bool
+        """
+        try:
+            if recursive:
+                self.__commandsInterface.rmrf(dirname, recursive=True,
+                                              force=True)
+            else:
+                self.__commandsInterface.rmdir(dirname)
+            self.removeDirectoryDone.emit()
+        except Exception as exc:
+            self.error.emit("rmdir", str(exc))
+    
+    def fileSystemInfo(self):
+        """
+        Public method to obtain information about the currently mounted file
+        systems.
+        """
+        try:
+            fsinfo = self.__commandsInterface.fileSystemInfo()
+            self.fsinfoDone.emit(fsinfo)
+        except Exception as exc:
+            self.error.emit("fileSystemInfo", str(exc))
+    
+    ##################################################################
+    ## some non-filesystem related methods below
+    ##################################################################
+    
+    @pyqtSlot()
+    def synchronizeTime(self):
+        """
+        Public slot to set the time of the connected device to the local
+        computer's time.
+        """
+        try:
+            self.__commandsInterface.syncTime()
+            self.synchTimeDone.emit()
+        except Exception as exc:
+            self.error.emit("rmdir", str(exc))
+    
+    @pyqtSlot()
+    def showTime(self):
+        """
+        Public slot to get the current date and time of the device.
+        """
+        try:
+            dt = self.__commandsInterface.showTime()
+            self.showTimeDone.emit(dt)
+        except Exception as exc:
+            self.error.emit("showTime", str(exc))
+    
+    @pyqtSlot()
+    def showVersion(self):
+        """
+        Public slot to get the version info for the MicroPython run by the
+        connected device.
+        """
+        try:
+            versionInfo = self.__commandsInterface.version()
+            self.showVersionDone.emit(versionInfo)
+        except Exception as exc:
+            self.error.emit("showVersion", str(exc))
+    
+    @pyqtSlot()
+    def showImplementation(self):
+        """
+        Public slot to obtain some implementation related information.
+        """
+        try:
+            impInfo = self.__commandsInterface.getImplementation()
+            if impInfo["name"] == "micropython":
+                name = "MicroPython"
+            elif impInfo["name"] == "circuitpython":
+                name = "CircuitPython"
+            elif impInfo["name"] == "unknown":
+                name = self.tr("unknown")
+            else:
+                name = impInfo["name"]
+            if impInfo["version"] == "unknown":
+                version = self.tr("unknown")
+            else:
+                version = impInfo["version"]
+            self.showImplementationDone.emit(name, version)
+        except Exception as exc:
+            self.error.emit("showVersion", str(exc))
--- a/eric6/MicroPython/MicroPythonFileManagerWidget.py	Sun Jul 28 18:55:00 2019 +0200
+++ b/eric6/MicroPython/MicroPythonFileManagerWidget.py	Mon Jul 29 20:20:18 2019 +0200
@@ -26,7 +26,7 @@
 
 from .Ui_MicroPythonFileManagerWidget import Ui_MicroPythonFileManagerWidget
 
-from .MicroPythonFileSystem import MicroPythonFileManager
+from .MicroPythonFileManager import MicroPythonFileManager
 from .MicroPythonFileSystemUtilities import (
     mtime2string, mode2string, decoratedName, listdirStat
 )
@@ -43,12 +43,12 @@
     """
     Class implementing a file manager for MicroPython devices.
     """
-    def __init__(self, port, parent=None):
+    def __init__(self, commandsInterface, parent=None):
         """
         Constructor
         
-        @param port port name of the device
-        @type str
+        @param commandsInterface reference to the commands interface object
+        @type MicroPythonCommandsInterface
         @param parent reference to the parent widget
         @type QWidget
         """
@@ -72,7 +72,7 @@
             0, Qt.AscendingOrder)
         
         self.__progressInfoDialog = None
-        self.__fileManager = MicroPythonFileManager(port, self)
+        self.__fileManager = MicroPythonFileManager(commandsInterface, self)
         
         self.__fileManager.longListFiles.connect(self.__handleLongListFiles)
         self.__fileManager.currentDir.connect(self.__handleCurrentDir)
@@ -144,15 +144,8 @@
         """
         Public method to start the widget.
         """
-        ui = e5App().getObject("UserInterface")
+        dirname = ""
         vm = e5App().getObject("ViewManager")
-        
-        ui.preferencesChanged.connect(
-            self.__fileManager.handlePreferencesChanged)
-        
-        self.__fileManager.connectToDevice()
-        
-        dirname = ""
         aw = vm.activeWindow()
         if aw:
             dirname = os.path.dirname(aw.getFileName())
@@ -167,11 +160,7 @@
         """
         Public method to stop the widget.
         """
-        ui = e5App().getObject("UserInterface")
-        ui.preferencesChanged.disconnect(
-            self.__fileManager.handlePreferencesChanged)
-        
-        self.__fileManager.disconnectFromDevice()
+        pass
     
     @pyqtSlot(str, str)
     def __handleError(self, method, error):
--- a/eric6/MicroPython/MicroPythonFileManagerWidget.ui	Sun Jul 28 18:55:00 2019 +0200
+++ b/eric6/MicroPython/MicroPythonFileManagerWidget.ui	Mon Jul 29 20:20:18 2019 +0200
@@ -11,31 +11,38 @@
    </rect>
   </property>
   <layout class="QGridLayout" name="gridLayout">
-   <property name="leftMargin">
-    <number>2</number>
-   </property>
-   <property name="topMargin">
-    <number>2</number>
-   </property>
-   <property name="rightMargin">
-    <number>2</number>
-   </property>
-   <property name="bottomMargin">
-    <number>2</number>
-   </property>
    <item row="0" column="0">
     <widget class="QLabel" name="label">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
      <property name="text">
       <string>Local Files</string>
      </property>
     </widget>
    </item>
    <item row="0" column="2">
-    <widget class="QLabel" name="label_2">
-     <property name="text">
-      <string>Device Files</string>
-     </property>
-    </widget>
+    <layout class="QHBoxLayout" name="horizontalLayout_3">
+     <item>
+      <widget class="QLabel" name="label_2">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <property name="text">
+        <string>Device Files</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="E5Led" name="deviceConnectedLed" native="true"/>
+     </item>
+    </layout>
    </item>
    <item row="1" column="0">
     <widget class="QTreeWidget" name="localFileTreeWidget">
@@ -236,6 +243,14 @@
    </item>
   </layout>
  </widget>
+ <customwidgets>
+  <customwidget>
+   <class>E5Led</class>
+   <extends>QWidget</extends>
+   <header>E5Gui/E5Led.h</header>
+   <container>1</container>
+  </customwidget>
+ </customwidgets>
  <tabstops>
   <tabstop>localFileTreeWidget</tabstop>
   <tabstop>deviceFileTreeWidget</tabstop>
--- a/eric6/MicroPython/MicroPythonFileSystem.py	Sun Jul 28 18:55:00 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1102 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (c) 2019 Detlev Offenbach <detlev@die-offenbachs.de>
-#
-
-"""
-Module implementing some file system commands for MicroPython.
-"""
-
-from __future__ import unicode_literals
-
-import ast
-import time
-import os
-import stat
-
-from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject, QThread
-
-from .MicroPythonSerialPort import MicroPythonSerialPort
-from .MicroPythonFileSystemUtilities import (
-    mtime2string, mode2string, decoratedName, listdirStat
-)
-
-import Preferences
-
-
-class MicroPythonFileSystem(QObject):
-    """
-    Class implementing some file system commands for MicroPython.
-    
-    Commands are provided to perform operations on the file system of a
-    connected MicroPython device. Supported commands are:
-    <ul>
-    <li>ls: directory listing</li>
-    <li>lls: directory listing with meta data</li>
-    <li>cd: change directory</li>
-    <li>pwd: get the current directory</li>
-    <li>put: copy a file to the connected device</li>
-    <li>get: get a file from the connected device</li>
-    <li>rm: remove a file from the connected device</li>
-    <li>rmrf: remove a file/directory recursively (like 'rm -rf' in bash)
-    <li>mkdir: create a new directory</li>
-    <li>rmdir: remove an empty directory</li>
-    </ul>
-    
-    There are additional commands related to time and version.
-    <ul>
-    <li>version: get version info about MicroPython</li>
-    <li>syncTime: synchronize the time of the connected device</li>
-    <li>showTime: show the current time of the connected device</li>
-    </ul>
-    """
-    def __init__(self, parent=None):
-        """
-        Constructor
-        
-        @param parent reference to the parent object
-        @type QObject
-        """
-        super(MicroPythonFileSystem, self).__init__(parent)
-        
-        self.__serial = None
-    
-    def setSerial(self, serial):
-        """
-        Public method to set the serial port to be used.
-        
-        Note: The serial port should be initialized and open already.
-        
-        @param serial open serial port
-        @type MicroPythonSerialPort
-        """
-        self.__serial = serial
-    
-    def __rawOn(self):
-        """
-        Private method to switch the connected device to 'raw' mode.
-        
-        Note: switching to raw mode is done with synchronous writes.
-        
-        @return flag indicating success
-        @@rtype bool
-        """
-        if not self.__serial:
-            return False
-        
-        rawReplMessage = b"raw REPL; CTRL-B to exit\r\n"
-        softRebootMessage = b"soft reboot\r\n"
-        
-        self.__serial.write(b"\x02")        # end raw mode if required
-        self.__serial.waitForBytesWritten()
-        for _i in range(3):
-            # CTRL-C three times to break out of loops
-            self.__serial.write(b"\r\x03")
-            self.__serial.waitForBytesWritten()
-            QThread.msleep(10)
-        self.__serial.readAll()             # read all data and discard it
-        self.__serial.write(b"\r\x01")      # send CTRL-A to enter raw mode
-        self.__serial.readUntil(rawReplMessage)
-        if self.__serial.hasTimedOut():
-            return False
-        self.__serial.write(b"\x04")        # send CTRL-D to soft reset
-        self.__serial.readUntil(softRebootMessage)
-        if self.__serial.hasTimedOut():
-            return False
-        
-        # some MicroPython devices seem to need to be convinced in some
-        # special way
-        data = self.__serial.readUntil(rawReplMessage)
-        if self.__serial.hasTimedOut():
-            return False
-        if not data.endswith(rawReplMessage):
-            self.__serial.write(b"\r\x01")  # send CTRL-A again
-            self.__serial.readUntil(rawReplMessage)
-            if self.__serial.hasTimedOut():
-                return False
-        self.__serial.readAll()             # read all data and discard it
-        return True
-    
-    def __rawOff(self):
-        """
-        Private method to switch 'raw' mode off.
-        """
-        if self.__serial:
-            self.__serial.write(b"\x02")    # send CTRL-B to cancel raw mode
-    
-    def __execute(self, commands):
-        """
-        Private method to send commands to the connected device and return the
-        result.
-        
-        If no serial connection is available, empty results will be returned.
-        
-        @param commands list of commands to be executed
-        @type str
-        @return tuple containing stdout and stderr output of the device
-        @rtype tuple of (bytes, bytes)
-        """
-        if not self.__serial:
-            return b"", b""
-        
-        result = bytearray()
-        err = b""
-        
-        ok = self.__rawOn()
-        if not ok:
-            return (
-                b"",
-                b"Could not switch to raw mode. Is the device switched on?"
-            )
-        
-        QThread.msleep(10)
-        for command in commands:
-            if command:
-                commandBytes = command.encode("utf-8")
-                self.__serial.write(commandBytes + b"\x04")
-                # read until prompt
-                response = self.__serial.readUntil(b"\x04>")
-                if self.__serial.hasTimedOut():
-                    return b"", b"Timeout while processing commands."
-                if b"\x04" in response[2:-2]:
-                    # split stdout, stderr
-                    out, err = response[2:-2].split(b"\x04")
-                    result += out
-                else:
-                    err = b"invalid response received: " + response
-                if err:
-                    return b"", err
-        QThread.msleep(10)
-        self.__rawOff()
-        
-        return bytes(result), err
-    
-    def __shortError(self, error):
-        """
-        Private method to create a shortened error message.
-        
-        @param error verbose error message
-        @type bytes
-        @return shortened error message
-        @rtype str
-        """
-        if error:
-            decodedError = error.decode("utf-8")
-            try:
-                return decodedError.split["\r\n"][-2]
-            except Exception:
-                return decodedError
-        return self.tr("Detected an error without indications.")
-    
-    ##################################################################
-    ## Methods below implement the file system commands
-    ##################################################################
-    
-    def ls(self, dirname=""):
-        """
-        Public method to get a directory listing of the connected device.
-        
-        @param dirname name of the directory to be listed
-        @type str
-        @return tuple containg the directory listing
-        @rtype tuple of str
-        @exception IOError raised to indicate an issue with the device
-        """
-        commands = [
-            "import os",
-            "print(os.listdir('{0}'))".format(dirname),
-        ]
-        out, err = self.__execute(commands)
-        if err:
-            raise IOError(self.__shortError(err))
-        return ast.literal_eval(out.decode("utf-8"))
-    
-    def lls(self, dirname="", fullstat=False):
-        """
-        Public method to get a long directory listing of the connected device
-        including meta data.
-        
-        @param dirname name of the directory to be listed
-        @type str
-        @param fullstat flag indicating to return the full stat() tuple
-        @type bool
-        @return list containing the directory listing with tuple entries of
-            the name and and a tuple of mode, size and time (if fullstat is
-            false) or the complete stat() tuple. 'None' is returned in case the
-            directory doesn't exist.
-        @rtype tuple of (str, tuple)
-        @exception IOError raised to indicate an issue with the device
-        """
-        commands = [
-            "import os",
-            "\n".join([
-                "def stat(filename):",
-                "    try:",
-                "        rstat = os.lstat(filename)",
-                "    except:",
-                "        rstat = os.stat(filename)",
-                "    return tuple(rstat)",
-            ]),
-            "\n".join([
-                "def listdir_stat(dirname):",
-                "    try:",
-                "        files = os.listdir(dirname)",
-                "    except OSError:",
-                "        return None",
-                "    if dirname in ('', '/'):",
-                "        return list((f, stat(f)) for f in files)",
-                "    return list((f, stat(dirname + '/' + f)) for f in files)",
-            ]),
-            "print(listdir_stat('{0}'))".format(dirname),
-        ]
-        out, err = self.__execute(commands)
-        if err:
-            raise IOError(self.__shortError(err))
-        fileslist = ast.literal_eval(out.decode("utf-8"))
-        if fileslist is None:
-            return None
-        else:
-            if fullstat:
-                return fileslist
-            else:
-                return [(f, (s[0], s[6], s[8])) for f, s in fileslist]
-    
-    def cd(self, dirname):
-        """
-        Public method to change the current directory on the connected device.
-        
-        @param dirname directory to change to
-        @type str
-        @exception IOError raised to indicate an issue with the device
-        """
-        assert dirname
-        
-        commands = [
-            "import os",
-            "os.chdir('{0}')".format(dirname),
-        ]
-        out, err = self.__execute(commands)
-        if err:
-            raise IOError(self.__shortError(err))
-    
-    def pwd(self):
-        """
-        Public method to get the current directory of the connected device.
-        
-        @return current directory
-        @rtype str
-        @exception IOError raised to indicate an issue with the device
-        """
-        commands = [
-            "import os",
-            "print(os.getcwd())",
-        ]
-        out, err = self.__execute(commands)
-        if err:
-            raise IOError(self.__shortError(err))
-        return out.decode("utf-8").strip()
-     
-    def rm(self, filename):
-        """
-        Public method to remove a file from the connected device.
-        
-        @param filename name of the file to be removed
-        @type str
-        @exception IOError raised to indicate an issue with the device
-        """
-        assert filename
-        
-        commands = [
-            "import os",
-            "os.remove('{0}')".format(filename),
-        ]
-        out, err = self.__execute(commands)
-        if err:
-            raise IOError(self.__shortError(err))
-    
-    def rmrf(self, name, recursive=False, force=False):
-        """
-        Public method to remove a file or directory recursively.
-        
-        @param name of the file or directory to remove
-        @type str
-        @param recursive flag indicating a recursive deletion
-        @type bool
-        @param force flag indicating to ignore errors
-        @type bool
-        @return flag indicating success
-        @rtype bool
-        @exception IOError raised to indicate an issue with the device
-        """
-        assert name
-        
-        commands = [
-            "import os",
-            "\n".join([
-                "def remove_file(name, recursive=False, force=False):",
-                "    try:",
-                "        mode = os.stat(name)[0]",
-                "        if mode & 0x4000 != 0:",
-                "            if recursive:",
-                "                for file in os.listdir(name):",
-                "                    success = remove_file(name + '/' + file,"
-                " recursive, force)",
-                "                    if not success and not force:",
-                "                        return False",
-                "                os.rmdir(name)",
-                "            else:",
-                "                if not force:",
-                "                    return False",
-                "        else:",
-                "            os.remove(name)",
-                "    except:",
-                "        if not force:",
-                "            return False",
-                "    return True",
-            ]),
-            "print(remove_file('{0}', {1}, {2}))".format(name, recursive,
-                                                         force),
-        ]
-        out, err = self.__execute(commands)
-        if err:
-            raise IOError(self.__shortError(err))
-        return ast.literal_eval(out.decode("utf-8"))
-    
-    def mkdir(self, dirname):
-        """
-        Public method to create a new directory.
-        
-        @param dirname name of the directory to create
-        @type str
-        @exception IOError raised to indicate an issue with the device
-        """
-        assert dirname
-   
-        commands = [
-            "import os",
-            "os.mkdir('{0}')".format(dirname),
-        ]
-        out, err = self.__execute(commands)
-        if err:
-            raise IOError(self.__shortError(err))
-    
-    def rmdir(self, dirname):
-        """
-        Public method to remove a directory.
-        
-        @param dirname name of the directory to be removed
-        @type str
-        @exception IOError raised to indicate an issue with the device
-        """
-        assert dirname
-   
-        commands = [
-            "import os",
-            "os.rmdir('{0}')".format(dirname),
-        ]
-        out, err = self.__execute(commands)
-        if err:
-            raise IOError(self.__shortError(err))
-    
-    def put(self, hostFileName, deviceFileName=None):
-        """
-        Public method to copy a local file to the connected device.
-        
-        @param hostFileName name of the file to be copied
-        @type str
-        @param deviceFileName name of the file to copy to
-        @type str
-        @return flag indicating success
-        @rtype bool
-        @exception IOError raised to indicate an issue with the device
-        """
-        if not os.path.isfile(hostFileName):
-            raise IOError("No such file: {0}".format(hostFileName))
-        
-        with open(hostFileName, "rb") as hostFile:
-            content = hostFile.read()
-            # convert eol '\r'
-            content = content.replace(b"\r\n", b"\r")
-            content = content.replace(b"\n", b"\r")
-        
-        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=None):
-        """
-        Public method to copy a file from the connected device.
-        
-        @param deviceFileName name of the file to copy
-        @type str
-        @param hostFileName name of the file to copy to
-        @type str
-        @return flag indicating success
-        @rtype bool
-        @exception IOError raised to indicate an issue with the device
-        """
-        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
-        # convert eol to "\n"
-        out = out.replace(b"\r\n", b"\n")
-        out = out.replace(b"\r", b"\n")
-        with open(hostFileName, "wb") as hostFile:
-            hostFile.write(out)
-        return True
-    
-    def fileSystemInfo(self):
-        """
-        Public method to obtain information about the currently mounted file
-        systems.
-        
-        @return tuple of tuples containing the file system name, the total
-            size, the used size and the free size
-        @rtype tuple of tuples of (str, int, int, int)
-        @exception IOError raised to indicate an issue with the device
-        """
-        commands = [
-            "import os",
-            "\n".join([
-                "def fsinfo():",
-                "    infolist = []",
-                "    fsnames = os.listdir('/')",
-                "    for fs in fsnames:",
-                "        fs = '/' + fs",
-                "        infolist.append((fs, os.statvfs(fs)))",
-                "    return infolist",
-            ]),
-            "print(fsinfo())",
-        ]
-        out, err = self.__execute(commands)
-        if err:
-            raise IOError(self.__shortError(err))
-        infolist = ast.literal_eval(out.decode("utf-8"))
-        if infolist is None:
-            return None
-        else:
-            filesystemInfos = []
-            for fs, info in infolist:
-                totalSize = info[2] * info[1]
-                freeSize = info[4] * info[1]
-                usedSize = totalSize - freeSize
-                filesystemInfos.append((fs, totalSize, usedSize, freeSize))
-        
-        return tuple(filesystemInfos)
-    
-    ##################################################################
-    ## non-filesystem related methods below
-    ##################################################################
-    
-    def version(self):
-        """
-        Public method to get the MicroPython version information of the
-        connected device.
-        
-        @return dictionary containing the version information
-        @rtype dict
-        @exception IOError raised to indicate an issue with the device
-        """
-        commands = [
-            "import os",
-            "print(os.uname())",
-        ]
-        out, err = self.__execute(commands)
-        if err:
-            raise IOError(self.__shortError(err))
-        
-        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 getImplementation(self):
-        """
-        Public method to get some implementation information of the connected
-        device.
-        
-        @return dictionary containing the implementation information
-        @rtype dict
-        @exception IOError raised to indicate an issue with the device
-        """
-        commands = [
-            "import sys",
-            "res = {}",                             # __IGNORE_WARNING_M613__
-            "\n".join([
-                "try:",
-                "    res['name'] = sys.implementation.name",
-                "except AttributeError:",
-                "    res['name'] = 'unknown'",
-            ]),
-            "\n".join([
-                "try:",
-                "    res['version'] = '.'.join((str(i) for i in"
-                " sys.implementation.version))",
-                "except AttributeError:",
-                "    res['version'] = 'unknown'",
-            ]),
-            "print(res)",
-        ]
-        out, err = self.__execute(commands)
-        if err:
-            raise IOError(self.__shortError(err))
-        return ast.literal_eval(out.decode("utf-8"))
-    
-    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()",
-                "        clock_time = rtc_time[:6] + (rtc_time[6] + 1, 0)",
-                "        rtc.datetime(clock_time)",
-                "    except:",
-                "        try:",
-                "            import machine",
-                "            rtc = machine.RTC()",
-                "            try:",     # ESP8266 may use rtc.datetime()
-                "                clock_time = rtc_time[:6] +"
-                " (rtc_time[6] + 1, 0)",
-                "                rtc.datetime(clock_time)",
-                "            except:",  # ESP32 uses rtc.init()
-                "                rtc.init(rtc_time[:6])",
-                "        except:",
-                "            try:",
-                "                import rtc, time",
-                "                clock=rtc.RTC()",
-                "                clock.datetime = time.struct_time(rtc_time +"
-                " (-1, -1))",
-                "            except:",
-                "                pass",
-            ]),
-            "set_time({0})".format((now.tm_year, now.tm_mon, now.tm_mday,
-                                    now.tm_hour, now.tm_min, now.tm_sec,
-                                    now.tm_wday))
-        ]
-        out, err = self.__execute(commands)
-        if err:
-            raise IOError(self.__shortError(err))
-    
-    def showTime(self):
-        """
-        Public method to get the current time of the device.
-        
-        @return time of the device
-        @rtype str
-        @exception IOError raised to indicate an issue with the device
-        """
-        commands = [
-            "import time",
-            "print(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()))",
-            # __IGNORE_WARNING_M601__
-        ]
-        out, err = self.__execute(commands)
-        if err:
-            raise IOError(self.__shortError(err))
-        return out.decode("utf-8").strip()
-
-
-class MicroPythonFileManager(QObject):
-    """
-    Class implementing an interface to the device file system commands with
-    some additional sugar.
-    
-    @signal longListFiles(result) emitted with a tuple of tuples containing the
-        name, mode, size and time for each directory entry
-    @signal currentDir(dirname) emitted to report the current directory of the
-        device
-    @signal currentDirChanged(dirname) emitted to report back a change of the
-        current directory
-    @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 rsyncDone(localName, deviceName) emitted after the rsync operation
-        has been completed
-    @signal rsyncProgressMessage(msg) emitted to send a message about what
-        rsync is doing
-    @signal removeDirectoryDone() emitted after a directory has been deleted
-    @signal createDirectoryDone() emitted after a directory was created
-    @signal fsinfoDone(fsinfo) emitted after the file system information was
-        obtained
-    
-    @signal synchTimeDone() emitted after the time was synchronizde to the
-        device
-    @signal showTimeDone(dateTime) emitted after the date and time was fetched
-        from the connected device
-    @signal showVersionDone(versionInfo) emitted after the version information
-        was fetched from the connected device
-    @signal showImplementationDone(name,version) emitted after the
-        implementation information has been obtained
-    
-    @signal error(exc) emitted with a failure message to indicate a failure
-        during the most recent operation
-    """
-    longListFiles = pyqtSignal(tuple)
-    currentDir = pyqtSignal(str)
-    currentDirChanged = pyqtSignal(str)
-    getFileDone = pyqtSignal(str, str)
-    putFileDone = pyqtSignal(str, str)
-    deleteFileDone = pyqtSignal(str)
-    rsyncDone = pyqtSignal(str, str)
-    rsyncProgressMessage = pyqtSignal(str)
-    removeDirectoryDone = pyqtSignal()
-    createDirectoryDone = pyqtSignal()
-    fsinfoDone = pyqtSignal(tuple)
-    
-    synchTimeDone = pyqtSignal()
-    showTimeDone = pyqtSignal(str)
-    showVersionDone = pyqtSignal(dict)
-    showImplementationDone = pyqtSignal(str, str)
-    
-    error = pyqtSignal(str, str)
-    
-    def __init__(self, port, parent=None):
-        """
-        Constructor
-        
-        @param port port name of the device
-        @type str
-        @param parent reference to the parent object
-        @type QObject
-        """
-        super(MicroPythonFileManager, self).__init__(parent)
-        
-        self.__serialPort = port
-        self.__serial = MicroPythonSerialPort(
-            timeout=Preferences.getMicroPython("SerialTimeout"),
-            parent=self)
-        self.__fs = MicroPythonFileSystem(parent=self)
-    
-    @pyqtSlot()
-    def connectToDevice(self):
-        """
-        Public slot to start the manager.
-        """
-        self.__serial.openSerialLink(self.__serialPort)
-        self.__fs.setSerial(self.__serial)
-    
-    @pyqtSlot()
-    def disconnectFromDevice(self):
-        """
-        Public slot to stop the thread.
-        """
-        self.__serial.closeSerialLink()
-    
-    @pyqtSlot()
-    def handlePreferencesChanged(self):
-        """
-        Public slot to handle a change of the preferences.
-        """
-        self.__serial.setTimeout(Preferences.getMicroPython("SerialTimeout"))
-    
-    @pyqtSlot(str)
-    def lls(self, dirname):
-        """
-        Public slot to get a long listing of the given directory.
-        
-        @param dirname name of the directory to list
-        @type str
-        """
-        try:
-            filesList = self.__fs.lls(dirname)
-            result = [(decoratedName(name, mode),
-                       mode2string(mode),
-                       str(size),
-                       mtime2string(time)) for
-                      name, (mode, size, time) in filesList]
-            self.longListFiles.emit(tuple(result))
-        except Exception as exc:
-            self.error.emit("lls", str(exc))
-    
-    @pyqtSlot()
-    def pwd(self):
-        """
-        Public slot to get the current directory of the device.
-        """
-        try:
-            pwd = self.__fs.pwd()
-            self.currentDir.emit(pwd)
-        except Exception as exc:
-            self.error.emit("pwd", str(exc))
-    
-    @pyqtSlot(str)
-    def cd(self, dirname):
-        """
-        Public slot to change the current directory of the device.
-        
-        @param dirname name of the desired current directory
-        @type str
-        """
-        try:
-            self.__fs.cd(dirname)
-            self.currentDirChanged.emit(dirname)
-        except Exception as exc:
-            self.error.emit("cd", 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.error.emit("get", 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.emit(hostFileName, deviceFileName)
-        except Exception as exc:
-            self.error.emit("put", 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.error.emit("delete", str(exc))
-    
-    def __rsync(self, hostDirectory, deviceDirectory, mirror=True):
-        """
-        Private method to synchronize a local directory to the device.
-        
-        @param hostDirectory name of the local directory
-        @type str
-        @param deviceDirectory name of the directory on the device
-        @type str
-        @param mirror flag indicating to mirror the local directory to
-            the device directory
-        @type bool
-        @return list of errors
-        @rtype list of str
-        """
-        errors = []
-        
-        if not os.path.isdir(hostDirectory):
-            return [self.tr(
-                "The given name '{0}' is not a directory or does not exist.")
-                .format(hostDirectory)
-            ]
-        
-        self.rsyncProgressMessage.emit(
-            self.tr("Synchronizing <b>{0}</b>.").format(deviceDirectory)
-        )
-        
-        sourceDict = {}
-        sourceFiles = listdirStat(hostDirectory)
-        for name, nstat in sourceFiles:
-            sourceDict[name] = nstat
-        
-        destinationDict = {}
-        try:
-            destinationFiles = self.__fs.lls(deviceDirectory, fullstat=True)
-        except Exception as exc:
-            return [str(exc)]
-        if destinationFiles is None:
-            # the destination directory does not exist
-            try:
-                self.__fs.mkdir(deviceDirectory)
-            except Exception as exc:
-                return [str(exc)]
-        else:
-            for name, nstat in destinationFiles:
-                destinationDict[name] = nstat
-        
-        destinationSet = set(destinationDict.keys())
-        sourceSet = set(sourceDict.keys())
-        toAdd = sourceSet - destinationSet                  # add to dev
-        toDelete = destinationSet - sourceSet               # delete from dev
-        toUpdate = destinationSet.intersection(sourceSet)   # update files
-        
-        for sourceBasename in toAdd:
-            # name exists in source but not in device
-            sourceFilename = os.path.join(hostDirectory, sourceBasename)
-            destFilename = deviceDirectory + "/" + sourceBasename
-            self.rsyncProgressMessage.emit(
-                self.tr("Adding <b>{0}</b>...").format(destFilename))
-            if os.path.isfile(sourceFilename):
-                try:
-                    self.__fs.put(sourceFilename, destFilename)
-                except Exception as exc:
-                    # just note issues but ignore them otherwise
-                    errors.append(str(exc))
-            if os.path.isdir(sourceFilename):
-                # recurse
-                errs = self.__rsync(sourceFilename, destFilename,
-                                    mirror=mirror)
-                # just note issues but ignore them otherwise
-                errors.extend(errs)
-        
-        if mirror:
-            for destBasename in toDelete:
-                # name exists in device but not local, delete
-                destFilename = deviceDirectory + "/" + destBasename
-                self.rsyncProgressMessage.emit(
-                    self.tr("Removing <b>{0}</b>...").format(destFilename))
-                try:
-                    self.__fs.rmrf(destFilename, recursive=True, force=True)
-                except Exception as exc:
-                    # just note issues but ignore them otherwise
-                    errors.append(str(exc))
-        
-        for sourceBasename in toUpdate:
-            # names exist in both; do an update
-            sourceStat = sourceDict[sourceBasename]
-            destStat = destinationDict[sourceBasename]
-            sourceFilename = os.path.join(hostDirectory, sourceBasename)
-            destFilename = deviceDirectory + "/" + sourceBasename
-            destMode = destStat[0]
-            if os.path.isdir(sourceFilename):
-                if stat.S_ISDIR(destMode):
-                    # both are directories => recurs
-                    errs = self.__rsync(sourceFilename, destFilename,
-                                        mirror=mirror)
-                    # just note issues but ignore them otherwise
-                    errors.extend(errs)
-                else:
-                    self.rsyncProgressMessage.emit(
-                        self.tr("Source <b>{0}</b> is a directory and"
-                                " destination <b>{1}</b> is a file. Ignoring"
-                                " it.")
-                        .format(sourceFilename, destFilename)
-                    )
-            else:
-                if stat.S_ISDIR(destMode):
-                    self.rsyncProgressMessage.emit(
-                        self.tr("Source <b>{0}</b> is a file and destination"
-                                " <b>{1}</b> is a directory. Ignoring it.")
-                        .format(sourceFilename, destFilename)
-                    )
-                else:
-                    if sourceStat[8] > destStat[8]:     # mtime
-                        self.rsyncProgressMessage.emit(
-                            self.tr("Updating <b>{0}</b>...")
-                            .format(destFilename)
-                        )
-                        try:
-                            self.__fs.put(sourceFilename, destFilename)
-                        except Exception as exc:
-                            errors.append(str(exc))
-        
-        self.rsyncProgressMessage.emit(
-            self.tr("Done synchronizing <b>{0}</b>.").format(deviceDirectory)
-        )
-        
-        return errors
-    
-    @pyqtSlot(str, str)
-    @pyqtSlot(str, str, bool)
-    def rsync(self, hostDirectory, deviceDirectory, mirror=True):
-        """
-        Public slot to synchronize a local directory to the device.
-        
-        @param hostDirectory name of the local directory
-        @type str
-        @param deviceDirectory name of the directory on the device
-        @type str
-        @param mirror flag indicating to mirror the local directory to
-            the device directory
-        @type bool
-        """
-        errors = self.__rsync(hostDirectory, deviceDirectory, mirror=mirror)
-        if errors:
-            self.error.emit("rsync", "\n".join(errors))
-        
-        self.rsyncDone.emit(hostDirectory, deviceDirectory)
-    
-    @pyqtSlot(str)
-    def mkdir(self, dirname):
-        """
-        Public slot to create a new directory.
-        
-        @param dirname name of the directory to create
-        @type str
-        """
-        try:
-            self.__fs.mkdir(dirname)
-            self.createDirectoryDone.emit()
-        except Exception as exc:
-            self.error.emit("mkdir", str(exc))
-    
-    @pyqtSlot(str)
-    @pyqtSlot(str, bool)
-    def rmdir(self, dirname, recursive=False):
-        """
-        Public slot to (recursively) remove a directory.
-        
-        @param dirname name of the directory to be removed
-        @type str
-        @param recursive flag indicating a recursive removal
-        @type bool
-        """
-        try:
-            if recursive:
-                self.__fs.rmrf(dirname, recursive=True, force=True)
-            else:
-                self.__fs.rmdir(dirname)
-            self.removeDirectoryDone.emit()
-        except Exception as exc:
-            self.error.emit("rmdir", str(exc))
-    
-    def fileSystemInfo(self):
-        """
-        Public method to obtain information about the currently mounted file
-        systems.
-        """
-        try:
-            fsinfo = self.__fs.fileSystemInfo()
-            self.fsinfoDone.emit(fsinfo)
-        except Exception as exc:
-            self.error.emit("fileSystemInfo", str(exc))
-    
-    ##################################################################
-    ## some non-filesystem related methods below
-    ##################################################################
-    
-    @pyqtSlot()
-    def synchronizeTime(self):
-        """
-        Public slot to set the time of the connected device to the local
-        computer's time.
-        """
-        try:
-            self.__fs.syncTime()
-            self.synchTimeDone.emit()
-        except Exception as exc:
-            self.error.emit("rmdir", str(exc))
-    
-    @pyqtSlot()
-    def showTime(self):
-        """
-        Public slot to get the current date and time of the device.
-        """
-        try:
-            dt = self.__fs.showTime()
-            self.showTimeDone.emit(dt)
-        except Exception as exc:
-            self.error.emit("showTime", str(exc))
-    
-    @pyqtSlot()
-    def showVersion(self):
-        """
-        Public slot to get the version info for the MicroPython run by the
-        connected device.
-        """
-        try:
-            versionInfo = self.__fs.version()
-            self.showVersionDone.emit(versionInfo)
-        except Exception as exc:
-            self.error.emit("showVersion", str(exc))
-    
-    @pyqtSlot()
-    def showImplementation(self):
-        """
-        Public slot to obtain some implementation related information.
-        """
-        try:
-            impInfo = self.__fs.getImplementation()
-            if impInfo["name"] == "micropython":
-                name = "MicroPython"
-            elif impInfo["name"] == "circuitpython":
-                name = "CircuitPython"
-            elif impInfo["name"] == "unknown":
-                name = self.tr("unknown")
-            else:
-                name = impInfo["name"]
-            if impInfo["version"] == "unknown":
-                version = self.tr("unknown")
-            else:
-                version = impInfo["version"]
-            self.showImplementationDone.emit(name, version)
-        except Exception as exc:
-            self.error.emit("showVersion", str(exc))
--- a/eric6/MicroPython/MicroPythonReplWidget.py	Sun Jul 28 18:55:00 2019 +0200
+++ b/eric6/MicroPython/MicroPythonReplWidget.py	Mon Jul 29 20:20:18 2019 +0200
@@ -11,20 +11,11 @@
 
 import re
 
-from PyQt5.QtCore import (
-    pyqtSlot, pyqtSignal, Qt, QPoint, QEvent, QIODevice, QTimer
-)
-from PyQt5.QtGui import (
-    QColor, QKeySequence, QTextCursor, QBrush
-)
+from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QPoint, QEvent
+from PyQt5.QtGui import QColor, QKeySequence, QTextCursor, QBrush
 from PyQt5.QtWidgets import (
     QWidget, QMenu, QApplication, QHBoxLayout, QSpacerItem, QSizePolicy
 )
-try:
-    from PyQt5.QtSerialPort import QSerialPort
-    HAS_QTSERIALPORT = True
-except ImportError:
-    HAS_QTSERIALPORT = False
 
 from E5Gui.E5ZoomWidget import E5ZoomWidget
 from E5Gui import E5MessageBox, E5FileDialog
@@ -39,6 +30,11 @@
 except ImportError:
     HAS_QTCHART = False
 from .MicroPythonFileManagerWidget import MicroPythonFileManagerWidget
+try:
+    from .MicroPythonCommandsInterface import MicroPythonCommandsInterface
+    HAS_QTSERIALPORT = True
+except ImportError:
+    HAS_QTSERIALPORT = False
 
 import Globals
 import UI.PixmapCache
@@ -139,6 +135,8 @@
 }
 
 
+# TODO: make wrapping an option for the repl edit
+# TODO: add a connect button or make the disconnect button with changing icon (see IRC)
 class MicroPythonReplWidget(QWidget, Ui_MicroPythonReplWidget):
     """
     Class implementing the MicroPython REPL widget.
@@ -198,10 +196,14 @@
         self.__zoomWidget.valueChanged.connect(self.__doZoom)
         self.__currentZoom = 0
         
-        self.__serial = None
+        self.__fileManagerWidget = None
+        
+        self.__interface = MicroPythonCommandsInterface(self)
         self.__device = None
+        self.__connected = False
         self.setConnected(False)
         
+        # TODO: replace these by checking the button states
         self.__replRunning = False
         self.__plotterRunning = False
         self.__fileManagerRunning = False
@@ -223,6 +225,8 @@
         self.replEdit.customContextMenuRequested.connect(
             self.__showContextMenu)
         self.__ui.preferencesChanged.connect(self.__handlePreferencesChanged)
+        self.__ui.preferencesChanged.connect(
+            self.__interface.handlePreferencesChanged)
         
         self.__handlePreferencesChanged()
         
@@ -270,6 +274,15 @@
         self.replEdit.setFontFamily(self.__font.family())
         self.replEdit.setFontPointSize(self.__font.pointSize())
     
+    def commandsInterface(self):
+        """
+        Public method to get a reference to the commands interface object.
+        
+        @return reference to the commands interface object
+        @rtype MicroPythonCommandsInterface
+        """
+        return self.__interface
+    
     @pyqtSlot(int)
     def on_deviceTypeComboBox_activated(self, index):
         """
@@ -346,10 +359,11 @@
         @param connected connection state
         @type bool
         """
-        if connected:
-            self.deviceConnectedLed.setColor(QColor(Qt.green))
-        else:
-            self.deviceConnectedLed.setColor(QColor(Qt.red))
+        self.__connected = connected
+        
+        self.deviceConnectedLed.setOn(connected)
+        if self.__fileManagerWidget:
+            self.__fileManagerWidget.deviceConnectedLed.setOn(connected)
         
         self.deviceTypeComboBox.setEnabled(not connected)
         
@@ -369,22 +383,20 @@
                     """ the device's reset button and wait a few seconds"""
                     """ before trying again."""))
     
-    @pyqtSlot()
-    def on_replButton_clicked(self):
+    @pyqtSlot(bool)
+    def on_replButton_clicked(self, checked):
         """
-        Private slot to connect to the selected device and start a REPL.
+        Private slot to connect to enable or disable the REPL widget connecting
+        or disconnecting from the device.
+        
+        @param checked state of the button
+        @type bool
         """
         if not self.__device:
             self.__showNoDeviceMessage()
             return
         
-        if self.__replRunning:
-            self.dataReceived.disconnect(self.__processData)
-            if not self.__plotterRunning:
-                self.__disconnectSerial()
-            self.__replRunning = False
-            self.__device.setRepl(False)
-        else:
+        if checked:
             ok, reason = self.__device.canStartRepl()
             if not ok:
                 E5MessageBox.warning(
@@ -395,38 +407,33 @@
                 return
             
             self.replEdit.clear()
-            self.dataReceived.connect(self.__processData)
+            self.__interface.dataReceived.connect(self.__processData)
             
-            if not self.__serial:
-                self.__openSerialLink()
-                if self.__serial:
-                    if self.__device.forceInterrupt():
-                        # send a Ctrl-B (exit raw mode)
-                        self.__serial.write(b'\x02')
-                        # send Ctrl-C (keyboard interrupt)
-                        self.__serial.write(b'\x03')
+            if not self.__interface.isConnected():
+                self.__connectToDevice()
+                if self.__device.forceInterrupt():
+                    # send a Ctrl-B (exit raw mode)
+                    self.__interface.write(b'\x02')
+                    # send Ctrl-C (keyboard interrupt)
+                    self.__interface.write(b'\x03')
             
             self.__replRunning = True
             self.__device.setRepl(True)
             self.replEdit.setFocus(Qt.OtherFocusReason)
+        else:
+            self.__interface.dataReceived.disconnect(self.__processData)
+            if not self.__plotterRunning and not self.__fileManagerRunning:
+                self.__disconnectFromDevice()
+            self.__replRunning = False
+            self.__device.setRepl(False)
+        self.replButton.setChecked(checked)
     
     @pyqtSlot()
     def on_disconnectButton_clicked(self):
         """
         Private slot to disconnect from the currently connected device.
         """
-        if self.__replRunning:
-            self.on_replButton_clicked()
-        
-        if self.__plotterRunning:
-            self.on_chartButton_clicked()
-    
-    def __disconnectSerial(self):
-        """
-        Private slot to disconnect the serial connection.
-        """
-        self.__closeSerialLink()
-        self.setConnected(False)
+        self.__disconnectFromDevice()
     
     @pyqtSlot()
     def __clear(self):
@@ -434,7 +441,7 @@
         Private slot to clear the REPL pane.
         """
         self.replEdit.clear()
-        self.__serial and self.__serial.write(b"\r")
+        self.__interface.isConnected() and self.__interface.write(b"\r")
     
     @pyqtSlot()
     def __paste(self):
@@ -447,7 +454,7 @@
             if pasteText:
                 pasteText = pasteText.replace('\n\r', '\r')
                 pasteText = pasteText.replace('\n', '\r')
-                self.__serial and self.__serial.write(
+                self.__interface.isConnected() and self.__interface.write(
                     pasteText.encode("utf-8"))
     
     def eventFilter(self, obj, evt):
@@ -501,7 +508,7 @@
                 tc = self.replEdit.textCursor()
                 tc.movePosition(QTextCursor.EndOfLine)
                 self.replEdit.setTextCursor(tc)
-            self.__serial and self.__serial.write(msg)
+            self.__interface.isConnected() and self.__interface.write(msg)
             return True
         
         else:
@@ -586,11 +593,6 @@
                     elif action == "m":
                         self.__setCharFormat(match.group(0)[:-1].split(";"),
                                              tc)
-##            elif data[index] == 10:     # \n
-##                tc.movePosition(QTextCursor.End)
-##                self.replEdit.setTextCursor(tc)
-##                self.replEdit.insertPlainText(chr(data[index]))
-##                self.__setCharFormat(["0"], tc)     # reset format after a \n
             else:
                 tc.deleteChar()
                 self.replEdit.setTextCursor(tc)
@@ -753,21 +755,12 @@
             # return with device path prepended
             return "/dev/{0}".format(portName)
     
-    def __openSerialLink(self):
+    def __connectToDevice(self):
         """
-        Private method to open a serial link to the selected device.
+        Private method to connect to the selected device.
         """
         port = self.__getCurrentPort()
-        self.__serial = QSerialPort()
-        self.__serial.setPortName(port)
-        if self.__serial.open(QIODevice.ReadWrite):
-            self.__serial.setDataTerminalReady(True)
-            # 115.200 baud, 8N1
-            self.__serial.setBaudRate(115200)
-            self.__serial.setDataBits(QSerialPort.Data8)
-            self.__serial.setParity(QSerialPort.NoParity)
-            self.__serial.setStopBits(QSerialPort.OneStop)
-            self.__serial.readyRead.connect(self.__readSerial)
+        if self.__interface.connectToDevice(port):
             self.setConnected(True)
         else:
             E5MessageBox.warning(
@@ -775,40 +768,13 @@
                 self.tr("Serial Device Connect"),
                 self.tr("""<p>Cannot connect to device at serial port"""
                         """ <b>{0}</b>.</p>""").format(port))
-            self.__serial = None
     
-    def __closeSerialLink(self):
-        """
-        Private method to close the open serial connection.
-        """
-        if self.__serial:
-            self.__serial.close()
-            self.__serial = None
-    
-    @pyqtSlot()
-    def __readSerial(self):
-        """
-        Private slot to read all available serial data and emit it with the
-        "dataReceived" signal for further processing.
+    def __disconnectFromDevice(self):
         """
-        data = bytes(self.__serial.readAll())
-        self.dataReceived.emit(data)
-    
-    def execute(self, commandsList):
+        Private method to disconnect from the device.
         """
-        Public method to execute a series of commands over a period of time.
-        
-        @param commandsList list of commands to be execute on the device
-        @type list of bytes
-        """
-        def remainingTask(commands):
-            self.execute(commands)
-        
-        if commandsList:
-            command = commandsList[0]
-            self.__serial.write(command)
-            remainder = commandsList[1:]
-            QTimer.singleShot(2, lambda: remainingTask(remainder))
+        self.__interface.disconnectFromDevice()
+        self.setConnected(False)
     
     @pyqtSlot()
     def on_runButton_clicked(self):
@@ -847,7 +813,8 @@
             return
         
         if not self.__replRunning:
-            self.on_replButton_clicked()
+            # switch on the REPL
+            self.on_replButton_clicked(True)
         if self.__replRunning:
             self.__device.runScript(script)
     
@@ -883,11 +850,14 @@
         if aw:
             aw.saveFileAs(workspace)
     
-    @pyqtSlot()
-    def on_chartButton_clicked(self):
+    @pyqtSlot(bool)
+    def on_chartButton_clicked(self, checked):
         """
         Private slot to open a chart view to plot data received from the
         connected device.
+        
+        @param checked state of the button
+        @type bool
         """
         if not HAS_QTCHART:
             # QtChart not available => fail silently
@@ -897,27 +867,7 @@
             self.__showNoDeviceMessage()
             return
         
-        if self.__plotterRunning:
-            if self.__chartWidget.isDirty():
-                res = E5MessageBox.okToClearData(
-                    self,
-                    self.tr("Unsaved Chart Data"),
-                    self.tr("""The chart contains unsaved data."""),
-                    self.__chartWidget.saveData)
-                if not res:
-                    # abort
-                    return
-            
-            self.dataReceived.disconnect(self.__chartWidget.processData)
-            self.__chartWidget.dataFlood.disconnect(self.handleDataFlood)
-            
-            if not self.__replRunning:
-                self.__disconnectSerial()
-            
-            self.__plotterRunning = False
-            self.__device.setPlotter(False)
-            self.__ui.removeSideWidget(self.__chartWidget)
-        else:
+        if checked:
             ok, reason = self.__device.canStartPlotter()
             if not ok:
                 E5MessageBox.warning(
@@ -928,25 +878,53 @@
                 return
             
             self.__chartWidget = MicroPythonGraphWidget(self)
-            self.dataReceived.connect(self.__chartWidget.processData)
-            self.__chartWidget.dataFlood.connect(self.handleDataFlood)
+            self.__interface.dataReceived.connect(
+                self.__chartWidget.processData)
+            self.__chartWidget.dataFlood.connect(
+                self.handleDataFlood)
             
             self.__ui.addSideWidget(self.__ui.BottomSide, self.__chartWidget,
                                     UI.PixmapCache.getIcon("chart"),
                                     self.tr("╬╝Py Chart"))
             self.__ui.showSideWidget(self.__chartWidget)
             
-            if not self.__serial:
-                self.__openSerialLink()
-                if self.__serial:
-                    if self.__device.forceInterrupt():
-                        # send a Ctrl-B (exit raw mode)
-                        self.__serial.write(b'\x02')
-                        # send Ctrl-C (keyboard interrupt)
-                        self.__serial.write(b'\x03')
+            if not self.__interface.isConnected():
+                self.__connectToDevice()
+                if self.__device.forceInterrupt():
+                    # send a Ctrl-B (exit raw mode)
+                    self.__interface.write(b'\x02')
+                    # send Ctrl-C (keyboard interrupt)
+                    self.__interface.write(b'\x03')
             
             self.__plotterRunning = True
             self.__device.setPlotter(True)
+        else:
+            if self.__chartWidget.isDirty():
+                res = E5MessageBox.okToClearData(
+                    self,
+                    self.tr("Unsaved Chart Data"),
+                    self.tr("""The chart contains unsaved data."""),
+                    self.__chartWidget.saveData)
+                if not res:
+                    # abort
+                    return
+            
+            self.__interface.dataReceived.disconnect(
+                self.__chartWidget.processData)
+            self.__chartWidget.dataFlood.disconnect(
+                self.handleDataFlood)
+            
+            if not self.__replRunning and not self.__fileManagerRunning:
+                self.__disconnectFromDevice()
+            
+            self.__plotterRunning = False
+            self.__device.setPlotter(False)
+            self.__ui.removeSideWidget(self.__chartWidget)
+            
+            self.__chartWidget.deleteLater()
+            self.__chartWidget = None
+        
+        self.chartButton.setChecked(checked)
     
     @pyqtSlot()
     def handleDataFlood(self):
@@ -956,22 +934,19 @@
         self.on_disconnectButton_clicked()
         self.__device.handleDataFlood()
     
-    @pyqtSlot()
-    def on_filesButton_clicked(self):
+    @pyqtSlot(bool)
+    def on_filesButton_clicked(self, checked):
         """
         Private slot to open a file manager window to the connected device.
+        
+        @param checked state of the button
+        @type bool
         """
         if not self.__device:
             self.__showNoDeviceMessage()
             return
         
-        if self.__fileManagerRunning:
-            self.__fileManagerWidget.stop()
-            self.__ui.removeSideWidget(self.__fileManagerWidget)
-            
-            self.__device.setFileManager(False)
-            self.__fileManagerRunning = False
-        else:
+        if checked:
             ok, reason = self.__device.canStartFileManager()
             if not ok:
                 E5MessageBox.warning(
@@ -981,8 +956,10 @@
                             """<p>Reason: {0}</p>""").format(reason))
                 return
             
-            port = self.__getCurrentPort()
-            self.__fileManagerWidget = MicroPythonFileManagerWidget(port, self)
+            if not self.__interface.isConnected():
+                self.__connectToDevice()
+            self.__fileManagerWidget = MicroPythonFileManagerWidget(
+                self.__interface, self)
             
             self.__ui.addSideWidget(self.__ui.BottomSide,
                                     self.__fileManagerWidget,
@@ -994,3 +971,11 @@
             self.__fileManagerRunning = True
             
             self.__fileManagerWidget.start()
+        else:
+            self.__fileManagerWidget.stop()
+            self.__ui.removeSideWidget(self.__fileManagerWidget)
+            
+            self.__device.setFileManager(False)
+            self.__fileManagerRunning = False
+            self.__fileManagerWidget.deleteLater()
+            self.__fileManagerWidget = None
--- a/eric6/MicroPython/MicroPythonReplWidget.ui	Sun Jul 28 18:55:00 2019 +0200
+++ b/eric6/MicroPython/MicroPythonReplWidget.ui	Mon Jul 29 20:20:18 2019 +0200
@@ -106,6 +106,9 @@
        <property name="toolTip">
         <string>Press to open a terminal (REPL) on the selected device</string>
        </property>
+       <property name="checkable">
+        <bool>true</bool>
+       </property>
       </widget>
      </item>
      <item>
@@ -113,6 +116,9 @@
        <property name="toolTip">
         <string>Press to open a file manager on the selected device (REPL must be disconnected first)</string>
        </property>
+       <property name="checkable">
+        <bool>true</bool>
+       </property>
       </widget>
      </item>
      <item>
@@ -120,6 +126,9 @@
        <property name="toolTip">
         <string>Press to open a chart window to display data receive from the selected device</string>
        </property>
+       <property name="checkable">
+        <bool>true</bool>
+       </property>
       </widget>
      </item>
      <item>

eric ide

mercurial