Mon, 29 Jul 2019 20:20:18 +0200
Refactored and improved the MicroPython code to be able to show the file manager and the REPL simultaneously.
--- 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>