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