Tue, 09 Jul 2019 19:49:41 +0200
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)