Continued implementing the MicroPython support. micropython

Wed, 10 Jul 2019 20:21:57 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Wed, 10 Jul 2019 20:21:57 +0200
branch
micropython
changeset 7059
a8fad276cbd5
parent 7058
bdd583f96e96
child 7061
9db5a73217bb

Continued implementing the MicroPython support.

eric6.e4p file | annotate | diff | comparison | revisions
eric6/MicroPython/CircuitPythonDevices.py file | annotate | diff | comparison | revisions
eric6/MicroPython/EspDevices.py file | annotate | diff | comparison | revisions
eric6/MicroPython/MicroPythonDevices.py file | annotate | diff | comparison | revisions
eric6/MicroPython/MicroPythonReplWidget.py file | annotate | diff | comparison | revisions
eric6/MicroPython/MicroPythonReplWidget.ui file | annotate | diff | comparison | revisions
eric6/MicroPython/MicrobitDevices.py file | annotate | diff | comparison | revisions
eric6/icons/default/adafruitDevice.png file | annotate | diff | comparison | revisions
eric6/icons/default/circuitPythonDevice.png file | annotate | diff | comparison | revisions
eric6/icons/default/esp32Device.png file | annotate | diff | comparison | revisions
--- a/eric6.e4p	Tue Jul 09 19:49:41 2019 +0200
+++ b/eric6.e4p	Wed Jul 10 20:21:57 2019 +0200
@@ -454,8 +454,11 @@
     <Source>eric6/IconEditor/__init__.py</Source>
     <Source>eric6/IconEditor/cursors/__init__.py</Source>
     <Source>eric6/IconEditor/cursors/cursors_rc.py</Source>
+    <Source>eric6/MicroPython/CircuitPythonDevices.py</Source>
+    <Source>eric6/MicroPython/EspDevices.py</Source>
     <Source>eric6/MicroPython/MicroPythonDevices.py</Source>
     <Source>eric6/MicroPython/MicroPythonReplWidget.py</Source>
+    <Source>eric6/MicroPython/MicrobitDevices.py</Source>
     <Source>eric6/MicroPython/__init__.py</Source>
     <Source>eric6/MultiProject/AddProjectDialog.py</Source>
     <Source>eric6/MultiProject/MultiProject.py</Source>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/MicroPython/CircuitPythonDevices.py	Wed Jul 10 20:21:57 2019 +0200
@@ -0,0 +1,165 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2019 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the device interface class for CircuitPython boards.
+"""
+
+from __future__ import unicode_literals
+
+import os
+import ctypes
+from subprocess import check_output
+
+from E5Gui import E5MessageBox
+
+from .MicroPythonDevices import MicroPythonDevice
+from .MicroPythonReplWidget import HAS_QTCHART
+
+import Globals
+
+
+class CircuitPythonDevice(MicroPythonDevice):
+    """
+    Class implementing the device for CircuitPython boards.
+    """
+    def __init__(self, microPythonWidget, parent=None):
+        """
+        Constructor
+        
+        @param microPythonWidget reference to the main MicroPython widget
+        @type MicroPythonReplWidget
+        @param parent reference to the parent object
+        @type QObject
+        """
+        super(CircuitPythonDevice, self).__init__(microPythonWidget, parent)
+        
+        self.__replActive = False
+        self.__plotterActive = False
+    
+    def setButtons(self):
+        """
+        Public method to enable the supported action buttons.
+        """
+        super(CircuitPythonDevice, self).setButtons()
+        self.microPython.setActionButtons(repl=True, chart=HAS_QTCHART)
+    
+    def forceInterrupt(self):
+        """
+        Public method to determine the need for an interrupt when opening the
+        serial connection.
+        
+        @return flag indicating an interrupt is needed
+        @rtype bool
+        """
+        return False
+    
+    def canStartRepl(self):
+        """
+        Public method to determine, if a REPL can be started.
+        
+        @return tuple containing a flag indicating it is safe to start a REPL
+            and a reason why it cannot.
+        @rtype tuple of (bool, str)
+        """
+        return True, ""
+    
+    def setRepl(self, on):
+        """
+        Public method to set the REPL status and dependent status.
+        
+        @param on flag indicating the active status
+        @type bool
+        """
+        self.__replActive = on
+    
+    def canStartPlotter(self):
+        """
+        Public method to determine, if a Plotter can be started.
+        
+        @return tuple containing a flag indicating it is safe to start a
+            Plotter and a reason why it cannot.
+        @rtype tuple of (bool, str)
+        """
+        return True, ""
+    
+    def setPlotter(self, on):
+        """
+        Public method to set the Plotter status and dependent status.
+        
+        @param on flag indicating the active status
+        @type bool
+        """
+        self.__plotterActive = on
+    
+    def getWorkspace(self):
+        """
+        Public method to get the workspace directory.
+        
+        @return workspace directory used for saving files
+        @rtype str
+        """
+        deviceDirectory = None
+        
+        # Attempts to find the path on the filesystem that represents the
+        # plugged in CIRCUITPY board.
+        if Globals.isWindowsPlatform():
+            # we are on a Windows platform
+            def getVolumeName(diskName):
+                """
+                Local function to determine the volume of a disk or device.
+                
+                Each disk or external device connected to windows has an
+                attribute called "volume name". This function returns the
+                volume name for the given disk/device.
+
+                Code from http://stackoverflow.com/a/12056414
+                """
+                volumeNameBuffer = ctypes.create_unicode_buffer(1024)
+                ctypes.windll.kernel32.GetVolumeInformationW(
+                    ctypes.c_wchar_p(diskName), volumeNameBuffer,
+                    ctypes.sizeof(volumeNameBuffer), None, None, None, None, 0)
+                return volumeNameBuffer.value
+            
+            #
+            # In certain circumstances, volumes are allocated to USB
+            # storage devices which cause a Windows popup to raise if their
+            # volume contains no media. Wrapping the check in SetErrorMode
+            # with SEM_FAILCRITICALERRORS (1) prevents this popup.
+            #
+            oldMode = ctypes.windll.kernel32.SetErrorMode(1)
+            try:
+                for disk in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ':
+                    path = '{}:\\'.format(disk)
+                    if (os.path.exists(path) and
+                            getVolumeName(path) == 'CIRCUITPY'):
+                        return path
+            finally:
+                ctypes.windll.kernel32.SetErrorMode(oldMode)
+        else:
+            # we are on a Linux or macOS platform
+            for mountCommand in ['mount', '/sbin/mount']:
+                try:
+                    mountOutput = check_output(mountCommand).splitlines()
+                    mountedVolumes = [x.split()[2] for x in mountOutput]
+                    for volume in mountedVolumes:
+                        if volume.endswith(b'CIRCUITPY'):
+                            deviceDirectory = volume.decode('utf-8')
+                except FileNotFoundError:
+                    next
+        
+        if deviceDirectory:
+            return deviceDirectory
+        else:
+            # return the default workspace and give the user a warning
+            E5MessageBox.warning(
+                self.microPythonWidget,
+                self.tr("Workspace Directory"),
+                self.tr("Python files for CircuitPython devices are stored on"
+                        " the device. Therefore, to edit these files you need"
+                        " to have the device plugged in. Until you plug in a"
+                        " device, the standard directory will be used."))
+            
+            return super(CircuitPythonDevice, self).getWorkspace()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/MicroPython/EspDevices.py	Wed Jul 10 20:21:57 2019 +0200
@@ -0,0 +1,145 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2019 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing some utility functions and the MicroPythonDevice base
+class.
+"""
+
+from __future__ import unicode_literals
+
+from .MicroPythonDevices import MicroPythonDevice
+from .MicroPythonReplWidget import HAS_QTCHART
+
+
+class EspDevice(MicroPythonDevice):
+    """
+    Class implementing the device for ESP32 and ESP8266 based boards.
+    """
+    def __init__(self, microPythonWidget, parent=None):
+        """
+        Constructor
+        
+        @param microPythonWidget reference to the main MicroPython widget
+        @type MicroPythonReplWidget
+        @param parent reference to the parent object
+        @type QObject
+        """
+        super(EspDevice, self).__init__(microPythonWidget, parent)
+        
+        self.__replActive = False
+        self.__fileManagerActive = False
+        self.__plotterActive = False
+    
+    def setButtons(self):
+        """
+        Public method to enable the supported action buttons.
+        """
+        super(EspDevice, self).setButtons()
+        self.microPython.setActionButtons(
+            run=True, repl=True, files=True, chart=HAS_QTCHART)
+    
+    def forceInterrupt(self):
+        """
+        Public method to determine the need for an interrupt when opening the
+        serial connection.
+        
+        @return flag indicating an interrupt is needed
+        @rtype bool
+        """
+        return True
+    
+    def canStartRepl(self):
+        """
+        Public method to determine, if a REPL can be started.
+        
+        @return tuple containing a flag indicating it is safe to start a REPL
+            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, ""
+    
+    def setRepl(self, on):
+        """
+        Public method to set the REPL status and dependent status.
+        
+        @param on flag indicating the active status
+        @type bool
+        """
+        self.__replActive = on
+        self.microPython.setActionButtons(files=not on)
+    
+    def canStartPlotter(self):
+        """
+        Public method to determine, if a Plotter can be started.
+        
+        @return tuple containing a flag indicating it is safe to start a
+            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, ""
+    
+    def setPlotter(self, on):
+        """
+        Public method to set the Plotter status and dependent status.
+        
+        @param on flag indicating the active status
+        @type bool
+        """
+        self.__plotterActive = on
+        self.microPython.setActionButtons(files=not on)
+    
+    def canRunScript(self):
+        """
+        Public method to determine, if a Plotter can be started.
+        
+        @return tuple containing a flag indicating it is safe to start a
+            Plotter and a reason why it cannot.
+        @rtype tuple of (bool, str)
+        """
+        return self.canStartRepl()
+    
+    def runScript(self, script):
+        """
+        Public method to run the given Python script.
+        
+        @param script script to be executed
+        @type str
+        """
+        pythonScript = script.split("\n")
+        self.sendCommands(pythonScript)
+    
+    # TODO: not yet implemented
+    def canStartFileManager(self):
+        """
+        Public method to determine, if a File Manager can be started.
+        
+        @return tuple containing a flag indicating it is safe to start a
+            File Manager and a reason why it cannot.
+        @rtype tuple of (bool, str)
+        """
+        return False, self.tr("File Manager is not supported by this device.")
+    
+    # TODO: not yet implemented
+    def setFileManager(self, on):
+        """
+        Public method to set the File Manager status and dependent status.
+        
+        @param on flag indicating the active status
+        @type bool
+        """
+        pass
--- a/eric6/MicroPython/MicroPythonDevices.py	Tue Jul 09 19:49:41 2019 +0200
+++ b/eric6/MicroPython/MicroPythonDevices.py	Wed Jul 10 20:21:57 2019 +0200
@@ -11,11 +11,12 @@
 from __future__ import unicode_literals
 
 import logging
+import os
 
 from PyQt5.QtCore import QObject
 
-import Globals
 import UI.PixmapCache
+import Preferences
 
 
 SupportedBoards = {
@@ -44,7 +45,7 @@
             (0x1209, 0x7102),       # Mini SAM M0
         ],
         "description": "CircuitPython Boards",
-        "icon": "adafruitDevice",
+        "icon": "circuitPythonDevice",
     },
     
     "bbc_microbit": {
@@ -135,8 +136,18 @@
     @return instantiated device interface
     @rtype MicroPythonDevice
     """
-    # TODO: not implemented yet
-    return MicroPythonDevice(microPythonWidget)
+    if deviceType == "esp":
+        from .EspDevices import EspDevice
+        return EspDevice(microPythonWidget)
+    elif deviceType == "circuitpython":
+        from .CircuitPythonDevices import CircuitPythonDevice
+        return CircuitPythonDevice(microPythonWidget)
+    elif deviceType == "bbc_microbit":
+        from .MicrobitDevices import MicrobitDevice
+        return MicrobitDevice(microPythonWidget)
+    else:
+        # nothing specific requested
+        return MicroPythonDevice(microPythonWidget)
 
 
 class MicroPythonDevice(QObject):
@@ -154,13 +165,14 @@
         """
         super(MicroPythonDevice, self).__init__(parent)
         
-        self.__microPython = microPythonWidget
+        self.microPython = microPythonWidget
     
     def setButtons(self):
         """
         Public method to enable the supported action buttons.
         """
-        self.__microPython.setActionButtons(
+        self.microPython.setActionButtons(
+            open=False, save=False,
             run=False, repl=False, files=False, chart=False)
     
     def forceInterrupt(self):
@@ -179,8 +191,9 @@
         
         @return tuple containing a flag indicating it is safe to start a REPL
             and a reason why it cannot.
+        @rtype tuple of (bool, str)
         """
-        return False, self.tr("Not implemented")
+        return False, self.tr("REPL is not supported by this device.")
     
     def setRepl(self, on):
         """
@@ -189,7 +202,65 @@
         @param on flag indicating the active status
         @type bool
         """
-        return
+        pass
+    
+    def canStartPlotter(self):
+        """
+        Public method to determine, if a Plotter can be started.
+        
+        @return tuple containing a flag indicating it is safe to start a
+            Plotter and a reason why it cannot.
+        @rtype tuple of (bool, str)
+        """
+        return False, self.tr("Plotter is not supported by this device.")
+    
+    def setPlotter(self, on):
+        """
+        Public method to set the Plotter status and dependent status.
+        
+        @param on flag indicating the active status
+        @type bool
+        """
+        pass
+    
+    def canRunScript(self):
+        """
+        Public method to determine, if a Plotter can be started.
+        
+        @return tuple containing a flag indicating it is safe to start a
+            Plotter and a reason why it cannot.
+        @rtype tuple of (bool, str)
+        """
+        return False, self.tr("Running scripts is not supported by this"
+                              " device.")
+    
+    def runScript(self, script):
+        """
+        Public method to run the given Python script.
+        
+        @param script script to be executed
+        @type str
+        """
+        pass
+    
+    def canStartFileManager(self):
+        """
+        Public method to determine, if a File Manager can be started.
+        
+        @return tuple containing a flag indicating it is safe to start a
+            File Manager and a reason why it cannot.
+        @rtype tuple of (bool, str)
+        """
+        return False, self.tr("File Manager is not supported by this device.")
+    
+    def setFileManager(self, on):
+        """
+        Public method to set the File Manager status and dependent status.
+        
+        @param on flag indicating the active status
+        @type bool
+        """
+        pass
     
     def getWorkspace(self):
         """
@@ -198,46 +269,27 @@
         @return workspace directory used for saving files
         @rtype str
         """
-        return ""
+        return (Preferences.getMultiProject("Workspace") or
+                os.path.expanduser("~"))
     
-    # TODO: are these needed?
-##    def findDevice(self, deviceType):
-##        """
-##        Public method to find the first device of a specific type.
-##        
-##        @param deviceType device type
-##        @type str
-##        @return tuple containing the port the device is connected to and its
-##            serial number
-##        @rtype tuple of (str, str)
-##        """
-##        from PyQt5.QtSerialPort import QSerialPortInfo
-##        
-##        availablePorts = QSerialPortInfo.availablePorts()
-##        for port in availablePorts:
-##            vid = port.vendorIdentifier()
-##            pid = port.productIdentifier()
-##            for board in SupportedBoards:
-##                if ((vid, pid) in SupportedBoards[board] or
-##                        (vid, None) in SupportedBoards[board]):
-##                    portName = port.portName()
-##                    serialNumber = port.serialNumber()
-##                    return (self.__portPath(portName), serialNumber)
-##        
-##        return (None, None)
-##    
-##    def __portPath(self, portName):
-##        """
-##        Private method to get the full path of a given port.
-##        
-##        @param portName name of the port the path shall be determined for
-##        @type str
-##        @return full port path
-##        @rtype str
-##        """
-##        if Globals.isWindowsPlatform():
-##            # return name unchanged
-##            return portName
-##        else:
-##            # assume Posix system (Linux or macOS)
-##            return "/dev/{0}".format(portName)
+    def sendCommands(self, commandsList):
+        """
+        Public method to send a list of commands to the device.
+        
+        @param commandsList list of commands to be sent to the device
+        @type list of str
+        """
+        rawOn = [       # sequence of commands to enter raw mode
+            b'\x02',
+            b'\r\x03',
+            b'\r\x03',
+            b'\r\x03',
+            b'\r\x01',
+        ]
+        newLine = [b'print("\\n")\r',]
+        commands = [c.encode("utf-8)") + b'\r' for c in commandsList]
+        commands.append(b'\r')
+        commands.append(b'\x04')
+        rawOff = [b'\x02']
+        commandSequence = rawOn + newLine + commands + rawOff
+        self.microPython.execute(commandSequence)
--- a/eric6/MicroPython/MicroPythonReplWidget.py	Tue Jul 09 19:49:41 2019 +0200
+++ b/eric6/MicroPython/MicroPythonReplWidget.py	Wed Jul 10 20:21:57 2019 +0200
@@ -11,7 +11,9 @@
 
 import re
 
-from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QPoint, QEvent, QIODevice
+from PyQt5.QtCore import (
+    pyqtSlot, pyqtSignal, Qt, QPoint, QEvent, QIODevice, QTimer
+)
 from PyQt5.QtGui import QColor, QKeySequence, QTextCursor
 from PyQt5.QtWidgets import (
     QWidget, QMenu, QApplication, QHBoxLayout, QSpacerItem, QSizePolicy)
@@ -20,9 +22,15 @@
     HAS_QTSERIALPORT = True
 except ImportError:
     HAS_QTSERIALPORT = False
+try:
+    from PyQt5.QtChart import QChart    # __IGNORE_WARNING__
+    HAS_QTCHART = True
+except ImportError:
+    HAS_QTCHART = False
 
 from E5Gui.E5ZoomWidget import E5ZoomWidget
-from E5Gui import E5MessageBox
+from E5Gui import E5MessageBox, E5FileDialog
+from E5Gui.E5Application import e5App
 
 from .Ui_MicroPythonReplWidget import Ui_MicroPythonReplWidget
 
@@ -59,6 +67,10 @@
         
         self.deviceIconLabel.setPixmap(MicroPythonDevices.getDeviceIcon(
             "", False))
+        
+        self.openButton.setIcon(UI.PixmapCache.getIcon("open"))
+        self.saveButton.setIcon(UI.PixmapCache.getIcon("fileSaveAs"))
+        
         self.checkButton.setIcon(UI.PixmapCache.getIcon("question"))
         self.runButton.setIcon(UI.PixmapCache.getIcon("start"))
         self.replButton.setIcon(UI.PixmapCache.getIcon("terminal"))
@@ -171,9 +183,13 @@
         Public method to set the enabled state of the various action buttons.
         
         @param kwargs keyword arguments containg the enabled states (keys are
-            'run', 'repl', 'files', 'chart'
+            'run', 'repl', 'files', 'chart', 'open', 'save'
         @type dict
         """
+        if "open" in kwargs:
+            self.openButton.setEnabled(kwargs["open"])
+        if "save" in kwargs:
+            self.saveButton.setEnabled(kwargs["save"])
         if "run" in kwargs:
             self.runButton.setEnabled(kwargs["run"])
         if "repl" in kwargs:
@@ -218,21 +234,27 @@
         
         self.disconnectButton.setEnabled(connected)
     
+    def __showNoDeviceMessage(self):
+        """
+        Private method to show a message dialog indicating a missing device.
+        """
+        E5MessageBox.critical(
+            self,
+            self.tr("No device attached"),
+            self.tr("""Please ensure the device is plugged into your"""
+                    """ computer and selected.\n\nIt must have a version"""
+                    """ of MicroPython (or CircuitPython) flashed onto"""
+                    """ it before anything will work.\n\nFinally press"""
+                    """ the device's reset button and wait a few seconds"""
+                    """ before trying again."""))
+    
     @pyqtSlot()
     def on_replButton_clicked(self):
         """
         Private slot to connect to the selected device and start a REPL.
         """
         if not self.__device:
-            E5MessageBox.critical(
-                self,
-                self.tr("No device attached"),
-                self.tr("""Please ensure the device is plugged inti your"""
-                        """ computer.\n\nIt must have a version of"""
-                        """ MicroPython (or CircuitPython) flashed onto it"""
-                        """ before the REPL will work.\n\nFinally press the"""
-                        """ device's reset button and wait a few seconds"""
-                        """ before trying again."""))
+            self.__showNoDeviceMessage()
             return
         
         if self.__replRunning:
@@ -260,6 +282,9 @@
                         self.__serial.write(b'\x02')
                         # send Ctrl-C (keyboard interrupt)
                         self.__serial.write(b'\x03')
+            
+            self.__replRunning = True
+            self.__device.setRepl(True)
     
     @pyqtSlot()
     def on_disconnectButton_clicked(self):
@@ -283,7 +308,6 @@
         Private method to activate a data plotter widget.
         """
         # TODO: not implemented yet
-        raise NotImplementedError
     
     def __deactivatePlotter(self):
         """
@@ -368,7 +392,7 @@
         while tc.movePosition(QTextCursor.Down):
             pass
         
-        index = 1
+        index = 0
         while index < len(data):
             if data[index] == 8:        # \b
                 tc.movePosition(QTextCursor.Left)
@@ -380,7 +404,7 @@
                   data[index + 1] == 91):
                 # VT100 cursor command detected: <Esc>[
                 index += 2      # move index to after the [
-                match = self.__vt100Re.search(data[index:].decaode("utf-8"))
+                match = self.__vt100Re.search(data[index:].decode("utf-8"))
                 if match:
                     # move to last position in control sequence
                     # ++ will be done at end of loop
@@ -494,3 +518,89 @@
         """
         data = bytes(self.__serial.readAll())
         self.dataReceived.emit(data)
+    
+    def execute(self, commandsList):
+        """
+        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
+        """
+        if commandsList:
+            command = commandsList[0]
+            self.__serial.write(command)
+            remainder = commandsList[1:]
+            remainingTask = lambda commands=remainder: self.execute(commands)
+            QTimer.singleShot(2, remainingTask)
+    
+    @pyqtSlot()
+    def on_runButton_clicked(self):
+        """
+        Private slot to execute the script of the active editor on the
+        selected device.
+        """
+        if not self.__device:
+            self.__showNoDeviceMessage()
+            return
+        
+        aw = e5App().getObject("ViewManager").activeWindow()
+        if aw is None:
+            E5MessageBox.critical(
+                self,
+                self.tr("Run Script"),
+                self.tr("""There is no editor open. Abort..."""))
+            return
+        
+        script = aw.text()
+        if not script:
+            E5MessageBox.critical(
+                self,
+                self.tr("Run Script"),
+                self.tr("""The current editor does not contain a script."""
+                        """ Abort..."""))
+            return
+        
+        ok, reason = self.__device.canRunScript()
+        if not ok:
+            E5MessageBox.warning(
+                self,
+                self.tr("Run Script"),
+                self.tr("""<p>Cannot run script.</p><p>Reason:"""
+                        """ {0}</p>""").format(reason))
+            return
+        
+        if not self.__replRunning:
+            self.on_replButton_clicked()
+        if self.__replRunning:
+            self.__device.runScript(script)
+    
+    @pyqtSlot()
+    def on_openButton_clicked(self):
+        """
+        Private slot to open a file of the connected device.
+        """
+        if not self.__device:
+            self.__showNoDeviceMessage()
+            return
+        
+        workspace = self.__device.getWorkspace()
+        fileName = E5FileDialog.getOpenFileName(
+            self,
+            self.tr("Open Python File"),
+            workspace,
+            self.tr("Python3 Files (*.py)"))
+        if fileName:
+            e5App().getObject("ViewManager").openSourceFile(fileName)
+    
+    @pyqtSlot()
+    def on_saveButton_clicked(self):
+        """
+        Private slot to save the current editor to the connected device.
+        """
+        if not self.__device:
+            self.__showNoDeviceMessage()
+            return
+        
+        workspace = self.__device.getWorkspace()
+        aw = e5App().getObject("ViewManager").activeWindow()
+        aw.saveFileAs(workspace)
--- a/eric6/MicroPython/MicroPythonReplWidget.ui	Tue Jul 09 19:49:41 2019 +0200
+++ b/eric6/MicroPython/MicroPythonReplWidget.ui	Wed Jul 10 20:21:57 2019 +0200
@@ -71,6 +71,30 @@
    <item>
     <layout class="QHBoxLayout" name="horizontalLayout_2">
      <item>
+      <widget class="QToolButton" name="openButton">
+       <property name="toolTip">
+        <string>Press to open a file of the connected device</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QToolButton" name="saveButton">
+       <property name="toolTip">
+        <string>Press to save the current editor to the connected device</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="Line" name="line">
+       <property name="lineWidth">
+        <number>2</number>
+       </property>
+       <property name="orientation">
+        <enum>Qt::Vertical</enum>
+       </property>
+      </widget>
+     </item>
+     <item>
       <widget class="QToolButton" name="runButton">
        <property name="toolTip">
         <string>Press to run the current script on the selected device</string>
@@ -139,6 +163,8 @@
  <tabstops>
   <tabstop>deviceTypeComboBox</tabstop>
   <tabstop>checkButton</tabstop>
+  <tabstop>openButton</tabstop>
+  <tabstop>saveButton</tabstop>
   <tabstop>runButton</tabstop>
   <tabstop>replButton</tabstop>
   <tabstop>filesButton</tabstop>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/MicroPython/MicrobitDevices.py	Wed Jul 10 20:21:57 2019 +0200
@@ -0,0 +1,124 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2019 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the device interface class for BBC micro:bit boards.
+"""
+
+from __future__ import unicode_literals
+
+from .MicroPythonDevices import MicroPythonDevice
+from .MicroPythonReplWidget import HAS_QTCHART
+
+
+class MicrobitDevice(MicroPythonDevice):
+    """
+    Class implementing the device for BBC micro:bit boards.
+    """
+    def __init__(self, microPythonWidget, parent=None):
+        """
+        Constructor
+        
+        @param microPythonWidget reference to the main MicroPython widget
+        @type MicroPythonReplWidget
+        @param parent reference to the parent object
+        @type QObject
+        """
+        super(MicrobitDevice, self).__init__(microPythonWidget, parent)
+        
+        self.__replActive = False
+        self.__fileManagerActive = False
+        self.__plotterActive = False
+    
+    def setButtons(self):
+        """
+        Public method to enable the supported action buttons.
+        """
+        super(MicrobitDevice, self).setButtons()
+        self.microPython.setActionButtons(
+            repl=True, files=True, chart=HAS_QTCHART)
+    
+    def forceInterrupt(self):
+        """
+        Public method to determine the need for an interrupt when opening the
+        serial connection.
+        
+        @return flag indicating an interrupt is needed
+        @rtype bool
+        """
+        return True
+    
+    def canStartRepl(self):
+        """
+        Public method to determine, if a REPL can be started.
+        
+        @return tuple containing a flag indicating it is safe to start a REPL
+            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, ""
+    
+    def setRepl(self, on):
+        """
+        Public method to set the REPL status and dependent status.
+        
+        @param on flag indicating the active status
+        @type bool
+        """
+        self.__replActive = on
+        self.microPython.setActionButtons(files=not on)
+    
+    def canStartPlotter(self):
+        """
+        Public method to determine, if a Plotter can be started.
+        
+        @return tuple containing a flag indicating it is safe to start a
+            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, ""
+    
+    def setPlotter(self, on):
+        """
+        Public method to set the Plotter status and dependent status.
+        
+        @param on flag indicating the active status
+        @type bool
+        """
+        self.__plotterActive = on
+        self.microPython.setActionButtons(files=not on)
+    
+    # TODO: not yet implemented
+    def canStartFileManager(self):
+        """
+        Public method to determine, if a File Manager can be started.
+        
+        @return tuple containing a flag indicating it is safe to start a
+            File Manager and a reason why it cannot.
+        @rtype tuple of (bool, str)
+        """
+        return False, self.tr("File Manager is not supported by this device.")
+    
+    # TODO: not yet implemented
+    def setFileManager(self, on):
+        """
+        Public method to set the File Manager status and dependent status.
+        
+        @param on flag indicating the active status
+        @type bool
+        """
+        pass
Binary file eric6/icons/default/adafruitDevice.png has changed
Binary file eric6/icons/default/circuitPythonDevice.png has changed
Binary file eric6/icons/default/esp32Device.png has changed

eric ide

mercurial