--- 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