Continued implementing the MicroPython support. micropython

Tue, 09 Jul 2019 19:49:41 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Tue, 09 Jul 2019 19:49:41 +0200
branch
micropython
changeset 7058
bdd583f96e96
parent 7054
fb84d8489bc1
child 7059
a8fad276cbd5

Continued implementing the MicroPython support.

eric6/MicroPython/MicroPythonDevices.py file | annotate | diff | comparison | revisions
eric6/MicroPython/MicroPythonReplWidget.py file | annotate | diff | comparison | revisions
--- a/eric6/MicroPython/MicroPythonDevices.py	Sun Jul 07 18:48:17 2019 +0200
+++ b/eric6/MicroPython/MicroPythonDevices.py	Tue Jul 09 19:49:41 2019 +0200
@@ -23,51 +23,33 @@
         "ids": [
             (0x1A86, 0x7523),       # HL-340
             (0x10C4, 0xEA60),       # CP210x
-            (0x0403, 0x6015),       # Sparkfun ESP32 VID, PID
+            (0x0403, 0x6015),       # Sparkfun ESP32
         ],
         "description": "ESP8266, ESP32",
         "icon": "esp32Device",
     },
     
-    "adafruit": {
+    "circuitpython": {
         "ids": [
-            (0x239A, 0x8015),       # Adafruit Feather M0 CircuitPython
-            (0x239A, 0x8023),       # Adafruit Feather M0 Express CircuitPython
-            (0x239A, 0x801B),       # Adafruit Feather M0 Express CircuitPython
-            (0x239A, 0x8014),       # Adafruit Metro M0 CircuitPython
-            (0x239A, 0x8019),       # Adafruit CircuitPlayground
-                                    #   Express CircuitPython
-            (0x239A, 0x801D),       # Adafruit Gemma M0
-            (0x239A, 0x801F),       # Adafruit Trinket M0
-            (0x239A, 0x8012),       # Adafruit ItsyBitsy M0
-            (0x239A, 0x8021),       # Adafruit Metro M4
-            (0x239A, 0x8025),       # Adafruit Feather RadioFruit
-            (0x239A, 0x8026),       # Adafruit Feather M4
-            (0x239A, 0x8028),       # Adafruit pIRKey M0
-            (0x239A, 0x802A),       # Adafruit Feather 52840
-            (0x239A, 0x802C),       # Adafruit Itsy M4
-            (0x239A, 0x802E),       # Adafruit CRICKit M0
-            (0x239A, 0xD1ED),       # Adafruit HalloWing M0
-            (0x239A, 0x8030),       # Adafruit NeoTrellis M4
-            (0x239A, 0x8032),       # Grand Central
             (0x2B04, 0xC00C),       # Particle Argon
             (0x2B04, 0xC00D),       # Particle Boron
             (0x2B04, 0xC00E),       # Particle Xenon
-            (0x239A, 0x8034),       # future board
-            (0x239A, 0x8036),       # future board
-            (0x239A, 0x8038),       # future board
-            (0x239A, 0x803A),       # future board
-            (0x239A, 0x803C),       # future board
-            (0x239A, 0x803E),       # future board
             (0x239A, None),         # Any Adafruit Boards
+            (0x1209, 0xBAB1),       # Electronic Cats Meow Meow
+            (0x1209, 0xBAB2),       # Electronic Cats CatWAN USBStick
+            (0x1209, 0xBAB3),       # Electronic Cats Bast Pro Mini M0
+            (0x1B4F, 0x8D22),       # SparkFun SAMD21 Mini Breakout
+            (0x1B4F, 0x8D23),       # SparkFun SAMD21 Dev Breakout
+            (0x1209, 0x2017),       # Mini SAM M4
+            (0x1209, 0x7102),       # Mini SAM M0
         ],
-        "description": "Adafruit CircuitPython",
+        "description": "CircuitPython Boards",
         "icon": "adafruitDevice",
     },
     
     "bbc_microbit": {
         "ids": [
-            (0x0D28, 0x0204),       # micro:bit USB VID, PID
+            (0x0D28, 0x0204),       # micro:bit
         ],
         "description": "BBC micro:bit",
         "icon": "microbitDevice",
@@ -142,79 +124,120 @@
         return UI.PixmapCache.getPixmap(iconName)
 
 
-def getDevice(deviceType):
+def getDevice(deviceType, microPythonWidget):
     """
     Public method to instantiate a specific MicroPython device interface.
     
     @param deviceType type of the device interface
     @type str
+    @param microPythonWidget reference to the main MicroPython widget
+    @type MicroPythonReplWidget
     @return instantiated device interface
     @rtype MicroPythonDevice
     """
     # TODO: not implemented yet
-    return None
+    return MicroPythonDevice(microPythonWidget)
 
 
 class MicroPythonDevice(QObject):
     """
     Base class for the more specific MicroPython devices.
     """
-    def __init__(self, parent=None):
+    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(MicroPythonDevice, self).__init__(parent)
+        
+        self.__microPython = microPythonWidget
     
-    def supportedActions(self):
+    def setButtons(self):
         """
-        Public method to get the names of the supported actions.
-        
-        @return tuple of supported actions out of "repl", "run", "files",
-            "chart"
-        @rtype tuple of str
+        Public method to enable the supported action buttons.
         """
-        return tuple()
+        self.__microPython.setActionButtons(
+            run=False, repl=False, files=False, chart=False)
     
-    def findDevice(self, deviceType):
+    def forceInterrupt(self):
         """
-        Public method to find the first device of a specific type.
+        Public method to determine the need for an interrupt when opening the
+        serial connection.
         
-        @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)
+        @return flag indicating an interrupt is needed
+        @rtype bool
+        """
+        return True
+    
+    def canStartRepl(self):
         """
-        from PyQt5.QtSerialPort import QSerialPortInfo
+        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.
+        """
+        return False, self.tr("Not implemented")
+    
+    def setRepl(self, on):
+        """
+        Public method to set the REPL status and dependent status.
         
-        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)
+        @param on flag indicating the active status
+        @type bool
+        """
+        return
+    
+    def getWorkspace(self):
+        """
+        Public method to get the workspace directory.
         
-        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
+        @return workspace directory used for saving files
         @rtype str
         """
-        if Globals.isWindowsPlatform():
-            # return name unchanged
-            return portName
-        else:
-            # assume Posix system (Linux or macOS)
-            return "/dev/{0}".format(portName)
+        return ""
+    
+    # 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)
--- a/eric6/MicroPython/MicroPythonReplWidget.py	Sun Jul 07 18:48:17 2019 +0200
+++ b/eric6/MicroPython/MicroPythonReplWidget.py	Tue Jul 09 19:49:41 2019 +0200
@@ -11,17 +11,18 @@
 
 import re
 
-from PyQt5.QtCore import pyqtSlot, Qt, QPoint, QEvent
+from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QPoint, QEvent, QIODevice
 from PyQt5.QtGui import QColor, QKeySequence, QTextCursor
 from PyQt5.QtWidgets import (
     QWidget, QMenu, QApplication, QHBoxLayout, QSpacerItem, QSizePolicy)
 try:
-    from PyQt5.QtSerialPort import QSerialPortInfo  # __IGNORE_WARNING__
+    from PyQt5.QtSerialPort import QSerialPort
     HAS_QTSERIALPORT = True
 except ImportError:
     HAS_QTSERIALPORT = False
 
 from E5Gui.E5ZoomWidget import E5ZoomWidget
+from E5Gui import E5MessageBox
 
 from .Ui_MicroPythonReplWidget import Ui_MicroPythonReplWidget
 
@@ -34,10 +35,18 @@
 class MicroPythonReplWidget(QWidget, Ui_MicroPythonReplWidget):
     """
     Class implementing the MicroPython REPL widget.
+    
+    @signal dataReceived(data) emitted to send data received via the serial
+        connection for further processing
     """
     ZoomMin = -10
     ZoomMax = 20
     
+    DeviceTypeRole = Qt.UserRole
+    DevicePortRole = Qt.UserRole + 1
+    
+    dataReceived = pyqtSignal(bytes)
+    
     def __init__(self, parent=None):
         """
         Constructor
@@ -80,6 +89,10 @@
         self.__device = None
         self.setConnected(False)
         
+        self.__replRunning = False
+        self.__plotterRunning = False
+        self.__fileManagerRunning = False
+        
         if not HAS_QTSERIALPORT:
             self.replEdit.setHtml(self.tr(
                 "<h3>The QtSerialPort package is not available.<br/>"
@@ -113,10 +126,17 @@
         if devices:
             self.deviceInfoLabel.setText(
                 self.tr("%n supported device(s) detected.", n=len(devices)))
+            
+            index = 0
             for device in sorted(devices):
+                index += 1
                 self.deviceTypeComboBox.addItem(
-                    self.tr("{0} at {1}".format(device[1], device[2])),
-                    device[0])
+                    self.tr("{0} at {1}".format(device[1], device[2])))
+                self.deviceTypeComboBox.setItemData(
+                    index, device[0], self.DeviceTypeRole)
+                self.deviceTypeComboBox.setItemData(
+                    index, device[2], self.DevicePortRole)
+                
         else:
             self.deviceInfoLabel.setText(
                 self.tr("No supported devices detected."))
@@ -131,16 +151,13 @@
         @param index index of the selected device
         @type int
         """
-        deviceType = self.deviceTypeComboBox.itemData(index)
+        deviceType = self.deviceTypeComboBox.itemData(
+            index, self.DeviceTypeRole)
         self.deviceIconLabel.setPixmap(MicroPythonDevices.getDeviceIcon(
             deviceType, False))
         
-        self.__device = MicroPythonDevices.getDevice(deviceType)
-        if self.__device:
-            actions = self.__device.supportedActions()
-        else:
-            actions = tuple()
-        self.__setActionButtons(actions)
+        self.__device = MicroPythonDevices.getDevice(deviceType, self)
+        self.__device.setButtons()
     
     @pyqtSlot()
     def on_checkButton_clicked(self):
@@ -149,18 +166,22 @@
         """
         self.__populateDeviceTypeComboBox()
     
-    def __setActionButtons(self, actions):
+    def setActionButtons(self, **kwargs):
         """
-        Private method to set the enabled state of the various action buttons.
+        Public method to set the enabled state of the various action buttons.
         
-        @param actions tuple of supported actions out of "repl", "run",
-            "files", "chart"
-        @type tuple of str
+        @param kwargs keyword arguments containg the enabled states (keys are
+            'run', 'repl', 'files', 'chart'
+        @type dict
         """
-        self.runButton.setEnabled("run" in actions)
-        self.replButton.setEnabled("repl" in actions)
-        self.filesButton.setEnabled("files" in actions)
-        self.chartButton.setEnabled("chart" in actions)
+        if "run" in kwargs:
+            self.runButton.setEnabled(kwargs["run"])
+        if "repl" in kwargs:
+            self.replButton.setEnabled(kwargs["repl"])
+        if "files" in kwargs:
+            self.filesButton.setEnabled(kwargs["files"])
+        if "chart" in kwargs:
+            self.chartButton.setEnabled(kwargs["chart"])
     
     @pyqtSlot(QPoint)
     def __showContextMenu(self, pos):
@@ -202,16 +223,60 @@
         """
         Private slot to connect to the selected device and start a REPL.
         """
-        # TODO: not implemented yet
-        raise NotImplementedError
+        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."""))
+            return
+        
+        if self.__replRunning:
+            self.dataReceived.disconnect(self.__processData)
+            self.on_disconnectButton_clicked()
+            self.__replRunning = False
+            self.__device.setRepl(False)
+        else:
+            ok, reason = self.__device.canStartRepl()
+            if not ok:
+                E5MessageBox.warning(
+                    self,
+                    self.tr("Start REPL"),
+                    self.tr("""<p>The REPL cannot be started.</p><p>Reason:"""
+                            """ {0}</p>""").format(reason))
+                return
+                
+            if not self.__serial:
+                self.replEdit.clear()
+                self.dataReceived.connect(self.__processData)
+                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')
     
     @pyqtSlot()
     def on_disconnectButton_clicked(self):
         """
         Private slot to disconnect from the currently connected device.
         """
-        # TODO: not implemented yet
-        raise NotImplementedError
+        if self.__replRunning:
+            self.on_replButton_clicked()
+        
+        # TODO: add more
+    
+    def __disconnect(self):
+        """
+        Private slot to disconnect the serial connection.
+        """
+        self.__closeSerialLink()
+        self.setConnected(False)
     
     def __activatePlotter(self):
         """
@@ -371,3 +436,61 @@
         elif value > self.__currentZoom:
             self.replEdit.zoomIn(value - self.__currentZoom)
         self.__currentZoom = value
+    
+    def __getCurrentPort(self):
+        """
+        Private method to determine the port path of the selected device.
+        
+        @return path of the port of the selected device
+        @rtype str
+        """
+        portName = self.deviceTypeComboBox.itemData(
+            self.deviceTypeComboBox.currentIndex(),
+            self.DevicePortRole)
+        
+        if Globals.isWindowsPlatform():
+            # return unchanged
+            return portName
+        else:
+            # return with device path prepended
+            return "/dev/{0}".format(portName)
+    
+    def __openSerialLink(self):
+        """
+        Private method to open a serial link 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)
+        else:
+            E5MessageBox.warning(
+                self,
+                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.
+        """
+        data = bytes(self.__serial.readAll())
+        self.dataReceived.emit(data)

eric ide

mercurial