eric6/MicroPython/MicroPythonReplWidget.py

branch
micropython
changeset 7095
8e10acb1cd85
parent 7094
d5f340dfb986
child 7103
aea236dc8002
diff -r d5f340dfb986 -r 8e10acb1cd85 eric6/MicroPython/MicroPythonReplWidget.py
--- 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

eric ide

mercurial