"Fossies" - the Fresh Open Source Software Archive

Member "eric6-20.9/eric/eric6/MicroPython/MicroPythonWidget.py" (4 Jul 2020, 58890 Bytes) of package /linux/misc/eric6-20.9.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "MicroPythonWidget.py" see the Fossies "Dox" file reference documentation.

    1 # -*- coding: utf-8 -*-
    2 
    3 # Copyright (c) 2019 - 2020 Detlev Offenbach <detlev@die-offenbachs.de>
    4 #
    5 
    6 """
    7 Module implementing the MicroPython REPL widget.
    8 """
    9 
   10 
   11 import re
   12 import time
   13 import os
   14 
   15 from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QPoint, QEvent
   16 from PyQt5.QtGui import QColor, QKeySequence, QTextCursor, QBrush
   17 from PyQt5.QtWidgets import (
   18     QWidget, QMenu, QApplication, QHBoxLayout, QSpacerItem, QSizePolicy,
   19     QTextEdit, QToolButton, QDialog
   20 )
   21 
   22 from E5Gui.E5ZoomWidget import E5ZoomWidget
   23 from E5Gui import E5MessageBox, E5FileDialog
   24 from E5Gui.E5Application import e5App
   25 from E5Gui.E5ProcessDialog import E5ProcessDialog
   26 
   27 from .Ui_MicroPythonWidget import Ui_MicroPythonWidget
   28 
   29 from . import MicroPythonDevices
   30 try:
   31     from .MicroPythonGraphWidget import MicroPythonGraphWidget
   32     HAS_QTCHART = True
   33 except ImportError:
   34     HAS_QTCHART = False
   35 from .MicroPythonFileManagerWidget import MicroPythonFileManagerWidget
   36 try:
   37     from .MicroPythonCommandsInterface import MicroPythonCommandsInterface
   38     HAS_QTSERIALPORT = True
   39 except ImportError:
   40     HAS_QTSERIALPORT = False
   41 
   42 import Globals
   43 import UI.PixmapCache
   44 import Preferences
   45 import Utilities
   46 
   47 # ANSI Colors (see https://en.wikipedia.org/wiki/ANSI_escape_code)
   48 AnsiColorSchemes = {
   49     "Windows 7": {
   50         0: QBrush(QColor(0, 0, 0)),
   51         1: QBrush(QColor(128, 0, 0)),
   52         2: QBrush(QColor(0, 128, 0)),
   53         3: QBrush(QColor(128, 128, 0)),
   54         4: QBrush(QColor(0, 0, 128)),
   55         5: QBrush(QColor(128, 0, 128)),
   56         6: QBrush(QColor(0, 128, 128)),
   57         7: QBrush(QColor(192, 192, 192)),
   58         10: QBrush(QColor(128, 128, 128)),
   59         11: QBrush(QColor(255, 0, 0)),
   60         12: QBrush(QColor(0, 255, 0)),
   61         13: QBrush(QColor(255, 255, 0)),
   62         14: QBrush(QColor(0, 0, 255)),
   63         15: QBrush(QColor(255, 0, 255)),
   64         16: QBrush(QColor(0, 255, 255)),
   65         17: QBrush(QColor(255, 255, 255)),
   66     },
   67     "Windows 10": {
   68         0: QBrush(QColor(12, 12, 12)),
   69         1: QBrush(QColor(197, 15, 31)),
   70         2: QBrush(QColor(19, 161, 14)),
   71         3: QBrush(QColor(193, 156, 0)),
   72         4: QBrush(QColor(0, 55, 218)),
   73         5: QBrush(QColor(136, 23, 152)),
   74         6: QBrush(QColor(58, 150, 221)),
   75         7: QBrush(QColor(204, 204, 204)),
   76         10: QBrush(QColor(118, 118, 118)),
   77         11: QBrush(QColor(231, 72, 86)),
   78         12: QBrush(QColor(22, 198, 12)),
   79         13: QBrush(QColor(249, 241, 165)),
   80         14: QBrush(QColor(59, 12, 255)),
   81         15: QBrush(QColor(180, 0, 158)),
   82         16: QBrush(QColor(97, 214, 214)),
   83         17: QBrush(QColor(242, 242, 242)),
   84     },
   85     "PuTTY": {
   86         0: QBrush(QColor(0, 0, 0)),
   87         1: QBrush(QColor(187, 0, 0)),
   88         2: QBrush(QColor(0, 187, 0)),
   89         3: QBrush(QColor(187, 187, 0)),
   90         4: QBrush(QColor(0, 0, 187)),
   91         5: QBrush(QColor(187, 0, 187)),
   92         6: QBrush(QColor(0, 187, 187)),
   93         7: QBrush(QColor(187, 187, 187)),
   94         10: QBrush(QColor(85, 85, 85)),
   95         11: QBrush(QColor(255, 85, 85)),
   96         12: QBrush(QColor(85, 255, 85)),
   97         13: QBrush(QColor(255, 255, 85)),
   98         14: QBrush(QColor(85, 85, 255)),
   99         15: QBrush(QColor(255, 85, 255)),
  100         16: QBrush(QColor(85, 255, 255)),
  101         17: QBrush(QColor(255, 255, 255)),
  102     },
  103     "xterm": {
  104         0: QBrush(QColor(0, 0, 0)),
  105         1: QBrush(QColor(205, 0, 0)),
  106         2: QBrush(QColor(0, 205, 0)),
  107         3: QBrush(QColor(205, 205, 0)),
  108         4: QBrush(QColor(0, 0, 238)),
  109         5: QBrush(QColor(205, 0, 205)),
  110         6: QBrush(QColor(0, 205, 205)),
  111         7: QBrush(QColor(229, 229, 229)),
  112         10: QBrush(QColor(127, 127, 127)),
  113         11: QBrush(QColor(255, 0, 0)),
  114         12: QBrush(QColor(0, 255, 0)),
  115         13: QBrush(QColor(255, 255, 0)),
  116         14: QBrush(QColor(0, 0, 255)),
  117         15: QBrush(QColor(255, 0, 255)),
  118         16: QBrush(QColor(0, 255, 255)),
  119         17: QBrush(QColor(255, 255, 255)),
  120     },
  121     "Ubuntu": {
  122         0: QBrush(QColor(1, 1, 1)),
  123         1: QBrush(QColor(222, 56, 43)),
  124         2: QBrush(QColor(57, 181, 74)),
  125         3: QBrush(QColor(255, 199, 6)),
  126         4: QBrush(QColor(0, 11, 184)),
  127         5: QBrush(QColor(118, 38, 113)),
  128         6: QBrush(QColor(44, 181, 233)),
  129         7: QBrush(QColor(204, 204, 204)),
  130         10: QBrush(QColor(128, 128, 128)),
  131         11: QBrush(QColor(255, 0, 0)),
  132         12: QBrush(QColor(0, 255, 0)),
  133         13: QBrush(QColor(255, 255, 0)),
  134         14: QBrush(QColor(0, 0, 255)),
  135         15: QBrush(QColor(255, 0, 255)),
  136         16: QBrush(QColor(0, 255, 255)),
  137         17: QBrush(QColor(255, 255, 255)),
  138     },
  139     "Ubuntu (dark)": {
  140         0: QBrush(QColor(96, 96, 96)),
  141         1: QBrush(QColor(235, 58, 45)),
  142         2: QBrush(QColor(57, 181, 74)),
  143         3: QBrush(QColor(255, 199, 29)),
  144         4: QBrush(QColor(25, 56, 230)),
  145         5: QBrush(QColor(200, 64, 193)),
  146         6: QBrush(QColor(48, 200, 255)),
  147         7: QBrush(QColor(204, 204, 204)),
  148         10: QBrush(QColor(128, 128, 128)),
  149         11: QBrush(QColor(255, 0, 0)),
  150         12: QBrush(QColor(0, 255, 0)),
  151         13: QBrush(QColor(255, 255, 0)),
  152         14: QBrush(QColor(0, 0, 255)),
  153         15: QBrush(QColor(255, 0, 255)),
  154         16: QBrush(QColor(0, 255, 255)),
  155         17: QBrush(QColor(255, 255, 255)),
  156     },
  157     "Breeze (dark)": {
  158         0: QBrush(QColor(35, 38, 39)),
  159         1: QBrush(QColor(237, 21, 21)),
  160         2: QBrush(QColor(17, 209, 22)),
  161         3: QBrush(QColor(246, 116, 0)),
  162         4: QBrush(QColor(29, 153, 243)),
  163         5: QBrush(QColor(155, 89, 182)),
  164         6: QBrush(QColor(26, 188, 156)),
  165         7: QBrush(QColor(252, 252, 252)),
  166         10: QBrush(QColor(127, 140, 141)),
  167         11: QBrush(QColor(192, 57, 43)),
  168         12: QBrush(QColor(28, 220, 154)),
  169         13: QBrush(QColor(253, 188, 75)),
  170         14: QBrush(QColor(61, 174, 233)),
  171         15: QBrush(QColor(142, 68, 173)),
  172         16: QBrush(QColor(22, 160, 133)),
  173         17: QBrush(QColor(255, 255, 255)),
  174     },
  175 }
  176 
  177 
  178 class MicroPythonWidget(QWidget, Ui_MicroPythonWidget):
  179     """
  180     Class implementing the MicroPython REPL widget.
  181     
  182     @signal dataReceived(data) emitted to send data received via the serial
  183         connection for further processing
  184     """
  185     ZoomMin = -10
  186     ZoomMax = 20
  187     
  188     DeviceTypeRole = Qt.UserRole
  189     DevicePortRole = Qt.UserRole + 1
  190     
  191     dataReceived = pyqtSignal(bytes)
  192     
  193     def __init__(self, parent=None):
  194         """
  195         Constructor
  196         
  197         @param parent reference to the parent widget
  198         @type QWidget
  199         """
  200         super(MicroPythonWidget, self).__init__(parent)
  201         self.setupUi(self)
  202         
  203         self.__ui = parent
  204         
  205         self.__superMenu = QMenu(self)
  206         self.__superMenu.aboutToShow.connect(self.__aboutToShowSuperMenu)
  207         
  208         self.menuButton.setObjectName(
  209             "micropython_supermenu_button")
  210         self.menuButton.setIcon(UI.PixmapCache.getIcon("superMenu"))
  211         self.menuButton.setToolTip(self.tr("MicroPython Menu"))
  212         self.menuButton.setPopupMode(QToolButton.InstantPopup)
  213         self.menuButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
  214         self.menuButton.setFocusPolicy(Qt.NoFocus)
  215         self.menuButton.setAutoRaise(True)
  216         self.menuButton.setShowMenuInside(True)
  217         self.menuButton.setMenu(self.__superMenu)
  218         
  219         self.deviceIconLabel.setPixmap(MicroPythonDevices.getDeviceIcon(
  220             "", False))
  221         
  222         self.openButton.setIcon(UI.PixmapCache.getIcon("open"))
  223         self.saveButton.setIcon(UI.PixmapCache.getIcon("fileSaveAs"))
  224         
  225         self.checkButton.setIcon(UI.PixmapCache.getIcon("question"))
  226         self.runButton.setIcon(UI.PixmapCache.getIcon("start"))
  227         self.replButton.setIcon(UI.PixmapCache.getIcon("terminal"))
  228         self.filesButton.setIcon(UI.PixmapCache.getIcon("filemanager"))
  229         self.chartButton.setIcon(UI.PixmapCache.getIcon("chart"))
  230         self.connectButton.setIcon(UI.PixmapCache.getIcon("linkConnect"))
  231         
  232         self.__zoomLayout = QHBoxLayout()
  233         spacerItem = QSpacerItem(40, 20, QSizePolicy.Expanding,
  234                                  QSizePolicy.Minimum)
  235         self.__zoomLayout.addSpacerItem(spacerItem)
  236         
  237         self.__zoom0 = self.replEdit.fontPointSize()
  238         self.__zoomWidget = E5ZoomWidget(
  239             UI.PixmapCache.getPixmap("zoomOut"),
  240             UI.PixmapCache.getPixmap("zoomIn"),
  241             UI.PixmapCache.getPixmap("zoomReset"), self)
  242         self.__zoomLayout.addWidget(self.__zoomWidget)
  243         self.layout().insertLayout(
  244             self.layout().count() - 1,
  245             self.__zoomLayout)
  246         self.__zoomWidget.setMinimum(self.ZoomMin)
  247         self.__zoomWidget.setMaximum(self.ZoomMax)
  248         self.__zoomWidget.valueChanged.connect(self.__doZoom)
  249         self.__currentZoom = 0
  250         
  251         self.__fileManagerWidget = None
  252         self.__chartWidget = None
  253         
  254         if HAS_QTSERIALPORT:
  255             self.__interface = MicroPythonCommandsInterface(self)
  256         else:
  257             self.__interface = None
  258         self.__device = None
  259         self.__connected = False
  260         self.__setConnected(False)
  261         
  262         if not HAS_QTSERIALPORT:
  263             self.replEdit.setHtml(self.tr(
  264                 "<h3>The QtSerialPort package is not available.<br/>"
  265                 "MicroPython support is deactivated.</h3>"))
  266             self.setEnabled(False)
  267             return
  268         
  269         self.__vt100Re = re.compile(
  270             r'(?P<count>\d*)(?P<color>(?:;?\d*)*)(?P<action>[ABCDKm])')
  271         
  272         self.__populateDeviceTypeComboBox()
  273         
  274         self.replEdit.installEventFilter(self)
  275         
  276         self.replEdit.customContextMenuRequested.connect(
  277             self.__showContextMenu)
  278         self.__ui.preferencesChanged.connect(self.__handlePreferencesChanged)
  279         self.__ui.preferencesChanged.connect(
  280             self.__interface.handlePreferencesChanged)
  281         
  282         self.__handlePreferencesChanged()
  283         
  284         charFormat = self.replEdit.currentCharFormat()
  285         self.DefaultForeground = charFormat.foreground()
  286         self.DefaultBackground = charFormat.background()
  287     
  288     def __populateDeviceTypeComboBox(self):
  289         """
  290         Private method to populate the device type selector.
  291         """
  292         currentDevice = self.deviceTypeComboBox.currentText()
  293         
  294         self.deviceTypeComboBox.clear()
  295         self.deviceInfoLabel.clear()
  296         
  297         self.deviceTypeComboBox.addItem("", "")
  298         devices, unknownDevices = MicroPythonDevices.getFoundDevices()
  299         if devices:
  300             self.deviceInfoLabel.setText(
  301                 self.tr("%n supported device(s) detected.", "", len(devices)))
  302             
  303             index = 0
  304             for device in sorted(devices):
  305                 index += 1
  306                 self.deviceTypeComboBox.addItem(
  307                     self.tr("{0} at {1}".format(device[1], device[2])))
  308                 self.deviceTypeComboBox.setItemData(
  309                     index, device[0], self.DeviceTypeRole)
  310                 self.deviceTypeComboBox.setItemData(
  311                     index, device[2], self.DevicePortRole)
  312                 
  313         else:
  314             self.deviceInfoLabel.setText(
  315                 self.tr("No supported devices detected."))
  316         
  317         index = self.deviceTypeComboBox.findText(currentDevice,
  318                                                  Qt.MatchExactly)
  319         if index == -1:
  320             # entry is no longer present
  321             index = 0
  322             if self.__connected:
  323                 # we are still connected, so disconnect
  324                 self.on_connectButton_clicked()
  325         
  326         self.on_deviceTypeComboBox_activated(index)
  327         self.deviceTypeComboBox.setCurrentIndex(index)
  328         
  329         if unknownDevices:
  330             ignoredUnknown = {
  331                 tuple(d)
  332                 for d in Preferences.getMicroPython("IgnoredUnknownDevices")
  333             }
  334             newUnknownDevices = set(unknownDevices) - ignoredUnknown
  335             if newUnknownDevices:
  336                 button = E5MessageBox.information(
  337                     self,
  338                     self.tr("Unknown MicroPython Device"),
  339                     self.tr(
  340                         '<p>Detected these unknown serial devices</p>'
  341                         '<ul>'
  342                         '<li>{0}</li>'
  343                         '</ul>'
  344                         '<p>Please report them together with the board name'
  345                         ' and a short description to <a href="mailto:'
  346                         'eric-bugs@eric-ide.python-projects.org"> the eric'
  347                         ' bug reporting address</a> if it is a MicroPython'
  348                         ' board.</p>'
  349                     ).format("</li><li>".join([
  350                         self.tr("{0} ({1:04x}/{2:04x})").format(desc, vid, pid)
  351                         for vid, pid, desc in newUnknownDevices])),
  352                     E5MessageBox.StandardButtons(
  353                         E5MessageBox.Ignore |
  354                         E5MessageBox.Ok
  355                     )
  356                 )
  357                 if button == E5MessageBox.Ignore:
  358                     ignoredUnknown = list(ignoredUnknown | newUnknownDevices)
  359                     Preferences.setMicroPython("IgnoredUnknownDevices",
  360                                                ignoredUnknown)
  361     
  362     def __handlePreferencesChanged(self):
  363         """
  364         Private slot to handle a change in preferences.
  365         """
  366         self.__colorScheme = Preferences.getMicroPython("ColorScheme")
  367         
  368         self.__font = Preferences.getEditorOtherFonts("MonospacedFont")
  369         self.replEdit.setFontFamily(self.__font.family())
  370         self.replEdit.setFontPointSize(self.__font.pointSize())
  371         
  372         if Preferences.getMicroPython("ReplLineWrap"):
  373             self.replEdit.setLineWrapMode(QTextEdit.WidgetWidth)
  374         else:
  375             self.replEdit.setLineWrapMode(QTextEdit.NoWrap)
  376         
  377         if self.__chartWidget is not None:
  378             self.__chartWidget.preferencesChanged()
  379     
  380     def commandsInterface(self):
  381         """
  382         Public method to get a reference to the commands interface object.
  383         
  384         @return reference to the commands interface object
  385         @rtype MicroPythonCommandsInterface
  386         """
  387         return self.__interface
  388     
  389     def isMicrobit(self):
  390         """
  391         Public method to check, if the connected/selected device is a
  392         BBC micro:bit.
  393         
  394         @return flag indicating a micro:bit device
  395         rtype bool
  396         """
  397         if self.__device and "micro:bit" in self.__device.deviceName():
  398             return True
  399         
  400         return False
  401     
  402     @pyqtSlot(int)
  403     def on_deviceTypeComboBox_activated(self, index):
  404         """
  405         Private slot handling the selection of a device type.
  406         
  407         @param index index of the selected device
  408         @type int
  409         """
  410         deviceType = self.deviceTypeComboBox.itemData(
  411             index, self.DeviceTypeRole)
  412         self.deviceIconLabel.setPixmap(MicroPythonDevices.getDeviceIcon(
  413             deviceType, False))
  414         
  415         self.__device = MicroPythonDevices.getDevice(deviceType, self)
  416         self.__device.setButtons()
  417         
  418         self.connectButton.setEnabled(bool(deviceType))
  419     
  420     @pyqtSlot()
  421     def on_checkButton_clicked(self):
  422         """
  423         Private slot to check for connected devices.
  424         """
  425         self.__populateDeviceTypeComboBox()
  426     
  427     def setActionButtons(self, **kwargs):
  428         """
  429         Public method to set the enabled state of the various action buttons.
  430         
  431         @keyparam kwargs keyword arguments containg the enabled states (keys
  432             are 'run', 'repl', 'files', 'chart', 'open', 'save'
  433         @type dict
  434         """
  435         if "open" in kwargs:
  436             self.openButton.setEnabled(kwargs["open"])
  437         if "save" in kwargs:
  438             self.saveButton.setEnabled(kwargs["save"])
  439         if "run" in kwargs:
  440             self.runButton.setEnabled(kwargs["run"])
  441         if "repl" in kwargs:
  442             self.replButton.setEnabled(kwargs["repl"])
  443         if "files" in kwargs:
  444             self.filesButton.setEnabled(kwargs["files"])
  445         if "chart" in kwargs:
  446             self.chartButton.setEnabled(kwargs["chart"] and HAS_QTCHART)
  447     
  448     @pyqtSlot(QPoint)
  449     def __showContextMenu(self, pos):
  450         """
  451         Private slot to show the REPL context menu.
  452         
  453         @param pos position to show the menu at
  454         @type QPoint
  455         """
  456         if Globals.isMacPlatform():
  457             copyKeys = QKeySequence(Qt.CTRL + Qt.Key_C)
  458             pasteKeys = QKeySequence(Qt.CTRL + Qt.Key_V)
  459         else:
  460             copyKeys = QKeySequence(Qt.CTRL + Qt.SHIFT + Qt.Key_C)
  461             pasteKeys = QKeySequence(Qt.CTRL + Qt.SHIFT + Qt.Key_V)
  462         menu = QMenu(self)
  463         menu.addAction(self.tr("Clear"), self.__clear)
  464         menu.addSeparator()
  465         menu.addAction(self.tr("Copy"), self.replEdit.copy, copyKeys)
  466         menu.addAction(self.tr("Paste"), self.__paste, pasteKeys)
  467         menu.addSeparator()
  468         menu.exec_(self.replEdit.mapToGlobal(pos))
  469     
  470     def __setConnected(self, connected):
  471         """
  472         Private method to set the connection status LED.
  473         
  474         @param connected connection state
  475         @type bool
  476         """
  477         self.__connected = connected
  478         
  479         self.deviceConnectedLed.setOn(connected)
  480         if self.__fileManagerWidget:
  481             self.__fileManagerWidget.deviceConnectedLed.setOn(connected)
  482         
  483         self.deviceTypeComboBox.setEnabled(not connected)
  484         
  485         if connected:
  486             self.connectButton.setIcon(
  487                 UI.PixmapCache.getIcon("linkDisconnect"))
  488             self.connectButton.setToolTip(self.tr(
  489                 "Press to disconnect the current device"))
  490         else:
  491             self.connectButton.setIcon(
  492                 UI.PixmapCache.getIcon("linkConnect"))
  493             self.connectButton.setToolTip(self.tr(
  494                 "Press to connect the selected device"))
  495     
  496     def isConnected(self):
  497         """
  498         Public method to get the connection state.
  499         
  500         @return connection state
  501         @rtype bool
  502         """
  503         return self.__connected
  504     
  505     def __showNoDeviceMessage(self):
  506         """
  507         Private method to show a message dialog indicating a missing device.
  508         """
  509         E5MessageBox.critical(
  510             self,
  511             self.tr("No device attached"),
  512             self.tr("""Please ensure the device is plugged into your"""
  513                     """ computer and selected.\n\nIt must have a version"""
  514                     """ of MicroPython (or CircuitPython) flashed onto"""
  515                     """ it before anything will work.\n\nFinally press"""
  516                     """ the device's reset button and wait a few seconds"""
  517                     """ before trying again."""))
  518     
  519     @pyqtSlot(bool)
  520     def on_replButton_clicked(self, checked):
  521         """
  522         Private slot to connect to enable or disable the REPL widget.
  523        
  524         If the selected device is not connected yet, this will be done now.
  525         
  526         @param checked state of the button
  527         @type bool
  528         """
  529         if not self.__device:
  530             self.__showNoDeviceMessage()
  531             return
  532         
  533         if checked:
  534             ok, reason = self.__device.canStartRepl()
  535             if not ok:
  536                 E5MessageBox.warning(
  537                     self,
  538                     self.tr("Start REPL"),
  539                     self.tr("""<p>The REPL cannot be started.</p><p>Reason:"""
  540                             """ {0}</p>""").format(reason))
  541                 return
  542             
  543             self.replEdit.clear()
  544             self.__interface.dataReceived.connect(self.__processData)
  545             
  546             if not self.__interface.isConnected():
  547                 self.__connectToDevice()
  548                 if self.__device.forceInterrupt():
  549                     # send a Ctrl-B (exit raw mode)
  550                     self.__interface.write(b'\x02')
  551                     # send Ctrl-C (keyboard interrupt)
  552                     self.__interface.write(b'\x03')
  553             
  554             self.__device.setRepl(True)
  555             self.replEdit.setFocus(Qt.OtherFocusReason)
  556         else:
  557             self.__interface.dataReceived.disconnect(self.__processData)
  558             if (not self.chartButton.isChecked() and
  559                     not self.filesButton.isChecked()):
  560                 self.__disconnectFromDevice()
  561             self.__device.setRepl(False)
  562         self.replButton.setChecked(checked)
  563     
  564     @pyqtSlot()
  565     def on_connectButton_clicked(self):
  566         """
  567         Private slot to connect to the selected device or disconnect from the
  568         currently connected device.
  569         """
  570         if self.__connected:
  571             self.__disconnectFromDevice()
  572             
  573             if self.replButton.isChecked():
  574                 self.on_replButton_clicked(False)
  575             if self.filesButton.isChecked():
  576                 self.on_filesButton_clicked(False)
  577             if self.chartButton.isChecked():
  578                 self.on_chartButton_clicked(False)
  579         else:
  580             self.__connectToDevice()
  581     
  582     @pyqtSlot()
  583     def __clear(self):
  584         """
  585         Private slot to clear the REPL pane.
  586         """
  587         self.replEdit.clear()
  588         self.__interface.isConnected() and self.__interface.write(b"\r")
  589     
  590     @pyqtSlot()
  591     def __paste(self):
  592         """
  593         Private slot to perform a paste operation.
  594         """
  595         clipboard = QApplication.clipboard()
  596         if clipboard:
  597             pasteText = clipboard.text()
  598             if pasteText:
  599                 pasteText = pasteText.replace('\n\r', '\r')
  600                 pasteText = pasteText.replace('\n', '\r')
  601                 self.__interface.isConnected() and self.__interface.write(
  602                     pasteText.encode("utf-8"))
  603     
  604     def eventFilter(self, obj, evt):
  605         """
  606         Public method to process events for the REPL pane.
  607         
  608         @param obj reference to the object the event was meant for
  609         @type QObject
  610         @param evt reference to the event object
  611         @type QEvent
  612         @return flag to indicate that the event was handled
  613         @rtype bool
  614         """
  615         if obj is self.replEdit and evt.type() == QEvent.KeyPress:
  616             # handle the key press event on behalve of the REPL pane
  617             key = evt.key()
  618             msg = bytes(evt.text(), 'utf8')
  619             if key == Qt.Key_Backspace:
  620                 msg = b'\b'
  621             elif key == Qt.Key_Delete:
  622                 msg = b'\x1B[\x33\x7E'
  623             elif key == Qt.Key_Up:
  624                 msg = b'\x1B[A'
  625             elif key == Qt.Key_Down:
  626                 msg = b'\x1B[B'
  627             elif key == Qt.Key_Right:
  628                 msg = b'\x1B[C'
  629             elif key == Qt.Key_Left:
  630                 msg = b'\x1B[D'
  631             elif key == Qt.Key_Home:
  632                 msg = b'\x1B[H'
  633             elif key == Qt.Key_End:
  634                 msg = b'\x1B[F'
  635             elif ((Globals.isMacPlatform() and
  636                    evt.modifiers() == Qt.MetaModifier) or
  637                   (not Globals.isMacPlatform() and
  638                    evt.modifiers() == Qt.ControlModifier)):
  639                 if Qt.Key_A <= key <= Qt.Key_Z:
  640                     # devices treat an input of \x01 as Ctrl+A, etc.
  641                     msg = bytes([1 + key - Qt.Key_A])
  642             elif (evt.modifiers() == Qt.ControlModifier | Qt.ShiftModifier or
  643                   (Globals.isMacPlatform() and
  644                    evt.modifiers() == Qt.ControlModifier)):
  645                 if key == Qt.Key_C:
  646                     self.replEdit.copy()
  647                     msg = b''
  648                 elif key == Qt.Key_V:
  649                     self.__paste()
  650                     msg = b''
  651             elif key in (Qt.Key_Return, Qt.Key_Enter):
  652                 tc = self.replEdit.textCursor()
  653                 tc.movePosition(QTextCursor.EndOfLine)
  654                 self.replEdit.setTextCursor(tc)
  655             self.__interface.isConnected() and self.__interface.write(msg)
  656             return True
  657         
  658         else:
  659             # standard event processing
  660             return super(MicroPythonWidget, self).eventFilter(obj, evt)
  661     
  662     def __processData(self, data):
  663         """
  664         Private slot to process bytes received from the device.
  665         
  666         @param data bytes received from the device
  667         @type bytes
  668         """
  669         tc = self.replEdit.textCursor()
  670         # the text cursor must be on the last line
  671         while tc.movePosition(QTextCursor.Down):
  672             pass
  673         
  674         # set the font
  675         charFormat = tc.charFormat()
  676         charFormat.setFontFamily(self.__font.family())
  677         charFormat.setFontPointSize(self.__font.pointSize())
  678         tc.setCharFormat(charFormat)
  679         
  680         index = 0
  681         while index < len(data):
  682             if data[index] == 8:        # \b
  683                 tc.movePosition(QTextCursor.Left)
  684                 self.replEdit.setTextCursor(tc)
  685             elif data[index] == 13:     # \r
  686                 pass
  687             elif (len(data) > index + 1 and
  688                   data[index] == 27 and
  689                   data[index + 1] == 91):
  690                 # VT100 cursor command detected: <Esc>[
  691                 index += 2      # move index to after the [
  692                 match = self.__vt100Re.search(data[index:].decode("utf-8"))
  693                 if match:
  694                     # move to last position in control sequence
  695                     # ++ will be done at end of loop
  696                     index += match.end() - 1
  697                     
  698                     action = match.group("action")
  699                     if action in "ABCD":
  700                         if match.group("count") == "":
  701                             count = 1
  702                         else:
  703                             count = int(match.group("count"))
  704                         
  705                         if action == "A":       # up
  706                             tc.movePosition(QTextCursor.Up, n=count)
  707                             self.replEdit.setTextCursor(tc)
  708                         elif action == "B":     # down
  709                             tc.movePosition(QTextCursor.Down, n=count)
  710                             self.replEdit.setTextCursor(tc)
  711                         elif action == "C":     # right
  712                             tc.movePosition(QTextCursor.Right, n=count)
  713                             self.replEdit.setTextCursor(tc)
  714                         elif action == "D":     # left
  715                             tc.movePosition(QTextCursor.Left, n=count)
  716                             self.replEdit.setTextCursor(tc)
  717                     elif action == "K":     # delete things
  718                         if match.group("count") in ("", "0"):
  719                             # delete to end of line
  720                             tc.movePosition(QTextCursor.EndOfLine,
  721                                             mode=QTextCursor.KeepAnchor)
  722                             tc.removeSelectedText()
  723                             self.replEdit.setTextCursor(tc)
  724                         elif match.group("count") == "1":
  725                             # delete to beinning of line
  726                             tc.movePosition(QTextCursor.StartOfLine,
  727                                             mode=QTextCursor.KeepAnchor)
  728                             tc.removeSelectedText()
  729                             self.replEdit.setTextCursor(tc)
  730                         elif match.group("count") == "2":
  731                             # delete whole line
  732                             tc.movePosition(QTextCursor.EndOfLine)
  733                             tc.movePosition(QTextCursor.StartOfLine,
  734                                             mode=QTextCursor.KeepAnchor)
  735                             tc.removeSelectedText()
  736                             self.replEdit.setTextCursor(tc)
  737                     elif action == "m":
  738                         self.__setCharFormat(match.group(0)[:-1].split(";"),
  739                                              tc)
  740             else:
  741                 tc.deleteChar()
  742                 self.replEdit.setTextCursor(tc)
  743                 self.replEdit.insertPlainText(chr(data[index]))
  744             
  745             index += 1
  746         
  747         self.replEdit.ensureCursorVisible()
  748     
  749     def __setCharFormat(self, formatCodes, textCursor):
  750         """
  751         Private method setting the current text format of the REPL pane based
  752         on the passed ANSI codes.
  753         
  754         Following codes are used:
  755         <ul>
  756         <li>0: Reset</li>
  757         <li>1: Bold font (weight 75)</li>
  758         <li>2: Light font (weight 25)</li>
  759         <li>3: Italic font</li>
  760         <li>4: Underlined font</li>
  761         <li>9: Strikeout font</li>
  762         <li>21: Bold off (weight 50)</li>
  763         <li>22: Light off (weight 50)</li>
  764         <li>23: Italic off</li>
  765         <li>24: Underline off</li>
  766         <li>29: Strikeout off</li>
  767         <li>30: foreground Black</li>
  768         <li>31: foreground Dark Red</li>
  769         <li>32: foreground Dark Green</li>
  770         <li>33: foreground Dark Yellow</li>
  771         <li>34: foreground Dark Blue</li>
  772         <li>35: foreground Dark Magenta</li>
  773         <li>36: foreground Dark Cyan</li>
  774         <li>37: foreground Light Gray</li>
  775         <li>39: reset foreground to default</li>
  776         <li>40: background Black</li>
  777         <li>41: background Dark Red</li>
  778         <li>42: background Dark Green</li>
  779         <li>43: background Dark Yellow</li>
  780         <li>44: background Dark Blue</li>
  781         <li>45: background Dark Magenta</li>
  782         <li>46: background Dark Cyan</li>
  783         <li>47: background Light Gray</li>
  784         <li>49: reset background to default</li>
  785         <li>53: Overlined font</li>
  786         <li>55: Overline off</li>
  787         <li>90: bright foreground Dark Gray</li>
  788         <li>91: bright foreground Red</li>
  789         <li>92: bright foreground Green</li>
  790         <li>93: bright foreground Yellow</li>
  791         <li>94: bright foreground Blue</li>
  792         <li>95: bright foreground Magenta</li>
  793         <li>96: bright foreground Cyan</li>
  794         <li>97: bright foreground White</li>
  795         <li>100: bright background Dark Gray</li>
  796         <li>101: bright background Red</li>
  797         <li>102: bright background Green</li>
  798         <li>103: bright background Yellow</li>
  799         <li>104: bright background Blue</li>
  800         <li>105: bright background Magenta</li>
  801         <li>106: bright background Cyan</li>
  802         <li>107: bright background White</li>
  803         </ul>
  804         
  805         @param formatCodes list of format codes
  806         @type list of str
  807         @param textCursor reference to the text cursor
  808         @type QTextCursor
  809         """
  810         if not formatCodes:
  811             # empty format codes list is treated as a reset
  812             formatCodes = ["0"]
  813         
  814         charFormat = textCursor.charFormat()
  815         for formatCode in formatCodes:
  816             try:
  817                 formatCode = int(formatCode)
  818             except ValueError:
  819                 # ignore non digit values
  820                 continue
  821             
  822             if formatCode == 0:
  823                 charFormat.setFontWeight(50)
  824                 charFormat.setFontItalic(False)
  825                 charFormat.setFontUnderline(False)
  826                 charFormat.setFontStrikeOut(False)
  827                 charFormat.setFontOverline(False)
  828                 charFormat.setForeground(self.DefaultForeground)
  829                 charFormat.setBackground(self.DefaultBackground)
  830             elif formatCode == 1:
  831                 charFormat.setFontWeight(75)
  832             elif formatCode == 2:
  833                 charFormat.setFontWeight(25)
  834             elif formatCode == 3:
  835                 charFormat.setFontItalic(True)
  836             elif formatCode == 4:
  837                 charFormat.setFontUnderline(True)
  838             elif formatCode == 9:
  839                 charFormat.setFontStrikeOut(True)
  840             elif formatCode in (21, 22):
  841                 charFormat.setFontWeight(50)
  842             elif formatCode == 23:
  843                 charFormat.setFontItalic(False)
  844             elif formatCode == 24:
  845                 charFormat.setFontUnderline(False)
  846             elif formatCode == 29:
  847                 charFormat.setFontStrikeOut(False)
  848             elif formatCode == 53:
  849                 charFormat.setFontOverline(True)
  850             elif formatCode == 55:
  851                 charFormat.setFontOverline(False)
  852             elif formatCode in (30, 31, 32, 33, 34, 35, 36, 37):
  853                 charFormat.setForeground(
  854                     AnsiColorSchemes[self.__colorScheme][formatCode - 30])
  855             elif formatCode in (40, 41, 42, 43, 44, 45, 46, 47):
  856                 charFormat.setBackground(
  857                     AnsiColorSchemes[self.__colorScheme][formatCode - 40])
  858             elif formatCode in (90, 91, 92, 93, 94, 95, 96, 97):
  859                 charFormat.setForeground(
  860                     AnsiColorSchemes[self.__colorScheme][formatCode - 80])
  861             elif formatCode in (100, 101, 102, 103, 104, 105, 106, 107):
  862                 charFormat.setBackground(
  863                     AnsiColorSchemes[self.__colorScheme][formatCode - 90])
  864             elif formatCode == 39:
  865                 charFormat.setForeground(self.DefaultForeground)
  866             elif formatCode == 49:
  867                 charFormat.setBackground(self.DefaultBackground)
  868         
  869         textCursor.setCharFormat(charFormat)
  870     
  871     def __doZoom(self, value):
  872         """
  873         Private slot to zoom the REPL pane.
  874         
  875         @param value zoom value
  876         @type int
  877         """
  878         if value < self.__currentZoom:
  879             self.replEdit.zoomOut(self.__currentZoom - value)
  880         elif value > self.__currentZoom:
  881             self.replEdit.zoomIn(value - self.__currentZoom)
  882         self.__currentZoom = value
  883     
  884     def getCurrentPort(self):
  885         """
  886         Public method to determine the port path of the selected device.
  887         
  888         @return path of the port of the selected device
  889         @rtype str
  890         """
  891         portName = self.deviceTypeComboBox.itemData(
  892             self.deviceTypeComboBox.currentIndex(),
  893             self.DevicePortRole)
  894         
  895         if Globals.isWindowsPlatform():
  896             # return it unchanged
  897             return portName
  898         else:
  899             # return with device path prepended
  900             return "/dev/{0}".format(portName)
  901     
  902     def getDeviceWorkspace(self):
  903         """
  904         Public method to get the workspace directory of the device.
  905         
  906         @return workspace directory of the device
  907         @rtype str
  908         """
  909         if self.__device:
  910             return self.__device.getWorkspace()
  911         else:
  912             return ""
  913     
  914     def __connectToDevice(self):
  915         """
  916         Private method to connect to the selected device.
  917         """
  918         port = self.getCurrentPort()
  919         if self.__interface.connectToDevice(port):
  920             self.__setConnected(True)
  921             
  922             if (Preferences.getMicroPython("SyncTimeAfterConnect") and
  923                     self.__device.hasTimeCommands()):
  924                 self.__synchronizeTime(quiet=True)
  925         else:
  926             E5MessageBox.warning(
  927                 self,
  928                 self.tr("Serial Device Connect"),
  929                 self.tr("""<p>Cannot connect to device at serial port"""
  930                         """ <b>{0}</b>.</p>""").format(port))
  931     
  932     def __disconnectFromDevice(self):
  933         """
  934         Private method to disconnect from the device.
  935         """
  936         self.__interface.disconnectFromDevice()
  937         self.__setConnected(False)
  938     
  939     @pyqtSlot()
  940     def on_runButton_clicked(self):
  941         """
  942         Private slot to execute the script of the active editor on the
  943         selected device.
  944         
  945         If the REPL is not active yet, it will be activated, which might cause
  946         an unconnected device to be connected.
  947         """
  948         if not self.__device:
  949             self.__showNoDeviceMessage()
  950             return
  951         
  952         aw = e5App().getObject("ViewManager").activeWindow()
  953         if aw is None:
  954             E5MessageBox.critical(
  955                 self,
  956                 self.tr("Run Script"),
  957                 self.tr("""There is no editor open. Abort..."""))
  958             return
  959         
  960         script = aw.text()
  961         if not script:
  962             E5MessageBox.critical(
  963                 self,
  964                 self.tr("Run Script"),
  965                 self.tr("""The current editor does not contain a script."""
  966                         """ Abort..."""))
  967             return
  968         
  969         ok, reason = self.__device.canRunScript()
  970         if not ok:
  971             E5MessageBox.warning(
  972                 self,
  973                 self.tr("Run Script"),
  974                 self.tr("""<p>Cannot run script.</p><p>Reason:"""
  975                         """ {0}</p>""").format(reason))
  976             return
  977         
  978         if not self.replButton.isChecked():
  979             # activate on the REPL
  980             self.on_replButton_clicked(True)
  981         if self.replButton.isChecked():
  982             self.__device.runScript(script)
  983     
  984     @pyqtSlot()
  985     def on_openButton_clicked(self):
  986         """
  987         Private slot to open a file of the connected device.
  988         """
  989         if not self.__device:
  990             self.__showNoDeviceMessage()
  991             return
  992         
  993         workspace = self.__device.getWorkspace()
  994         fileName = E5FileDialog.getOpenFileName(
  995             self,
  996             self.tr("Open Python File"),
  997             workspace,
  998             self.tr("Python3 Files (*.py);;All Files (*)"))
  999         if fileName:
 1000             e5App().getObject("ViewManager").openSourceFile(fileName)
 1001     
 1002     @pyqtSlot()
 1003     def on_saveButton_clicked(self):
 1004         """
 1005         Private slot to save the current editor to the connected device.
 1006         """
 1007         if not self.__device:
 1008             self.__showNoDeviceMessage()
 1009             return
 1010         
 1011         workspace = self.__device.getWorkspace()
 1012         aw = e5App().getObject("ViewManager").activeWindow()
 1013         if aw:
 1014             aw.saveFileAs(workspace)
 1015     
 1016     @pyqtSlot(bool)
 1017     def on_chartButton_clicked(self, checked):
 1018         """
 1019         Private slot to open a chart view to plot data received from the
 1020         connected device.
 1021        
 1022         If the selected device is not connected yet, this will be done now.
 1023         
 1024         @param checked state of the button
 1025         @type bool
 1026         """
 1027         if not HAS_QTCHART:
 1028             # QtChart not available => fail silently
 1029             return
 1030         
 1031         if not self.__device:
 1032             self.__showNoDeviceMessage()
 1033             return
 1034         
 1035         if checked:
 1036             ok, reason = self.__device.canStartPlotter()
 1037             if not ok:
 1038                 E5MessageBox.warning(
 1039                     self,
 1040                     self.tr("Start Chart"),
 1041                     self.tr("""<p>The Chart cannot be started.</p><p>Reason:"""
 1042                             """ {0}</p>""").format(reason))
 1043                 return
 1044             
 1045             self.__chartWidget = MicroPythonGraphWidget(self)
 1046             self.__interface.dataReceived.connect(
 1047                 self.__chartWidget.processData)
 1048             self.__chartWidget.dataFlood.connect(
 1049                 self.handleDataFlood)
 1050             
 1051             self.__ui.addSideWidget(self.__ui.BottomSide, self.__chartWidget,
 1052                                     UI.PixmapCache.getIcon("chart"),
 1053                                     self.tr("µPy Chart"))
 1054             self.__ui.showSideWidget(self.__chartWidget)
 1055             
 1056             if not self.__interface.isConnected():
 1057                 self.__connectToDevice()
 1058                 if self.__device.forceInterrupt():
 1059                     # send a Ctrl-B (exit raw mode)
 1060                     self.__interface.write(b'\x02')
 1061                     # send Ctrl-C (keyboard interrupt)
 1062                     self.__interface.write(b'\x03')
 1063             
 1064             self.__device.setPlotter(True)
 1065         else:
 1066             if self.__chartWidget.isDirty():
 1067                 res = E5MessageBox.okToClearData(
 1068                     self,
 1069                     self.tr("Unsaved Chart Data"),
 1070                     self.tr("""The chart contains unsaved data."""),
 1071                     self.__chartWidget.saveData)
 1072                 if not res:
 1073                     # abort
 1074                     return
 1075             
 1076             self.__interface.dataReceived.disconnect(
 1077                 self.__chartWidget.processData)
 1078             self.__chartWidget.dataFlood.disconnect(
 1079                 self.handleDataFlood)
 1080             
 1081             if (not self.replButton.isChecked() and
 1082                     not self.filesButton.isChecked()):
 1083                 self.__disconnectFromDevice()
 1084             
 1085             self.__device.setPlotter(False)
 1086             self.__ui.removeSideWidget(self.__chartWidget)
 1087             
 1088             self.__chartWidget.deleteLater()
 1089             self.__chartWidget = None
 1090         
 1091         self.chartButton.setChecked(checked)
 1092     
 1093     @pyqtSlot()
 1094     def handleDataFlood(self):
 1095         """
 1096         Public slot handling a data flood from the device.
 1097         """
 1098         self.on_connectButton_clicked()
 1099         self.__device.handleDataFlood()
 1100     
 1101     @pyqtSlot(bool)
 1102     def on_filesButton_clicked(self, checked):
 1103         """
 1104         Private slot to open a file manager window to the connected device.
 1105        
 1106         If the selected device is not connected yet, this will be done now.
 1107         
 1108         @param checked state of the button
 1109         @type bool
 1110         """
 1111         if not self.__device:
 1112             self.__showNoDeviceMessage()
 1113             return
 1114         
 1115         if checked:
 1116             ok, reason = self.__device.canStartFileManager()
 1117             if not ok:
 1118                 E5MessageBox.warning(
 1119                     self,
 1120                     self.tr("Start File Manager"),
 1121                     self.tr("""<p>The File Manager cannot be started.</p>"""
 1122                             """<p>Reason: {0}</p>""").format(reason))
 1123                 return
 1124             
 1125             if not self.__interface.isConnected():
 1126                 self.__connectToDevice()
 1127             self.__fileManagerWidget = MicroPythonFileManagerWidget(
 1128                 self.__interface, self.__device.supportsLocalFileAccess(),
 1129                 self)
 1130             
 1131             self.__ui.addSideWidget(self.__ui.BottomSide,
 1132                                     self.__fileManagerWidget,
 1133                                     UI.PixmapCache.getIcon("filemanager"),
 1134                                     self.tr("µPy Files"))
 1135             self.__ui.showSideWidget(self.__fileManagerWidget)
 1136 
 1137             self.__device.setFileManager(True)
 1138             
 1139             self.__fileManagerWidget.start()
 1140         else:
 1141             self.__fileManagerWidget.stop()
 1142             
 1143             if (not self.replButton.isChecked() and
 1144                     not self.chartButton.isChecked()):
 1145                 self.__disconnectFromDevice()
 1146             
 1147             self.__device.setFileManager(False)
 1148             self.__ui.removeSideWidget(self.__fileManagerWidget)
 1149             
 1150             self.__fileManagerWidget.deleteLater()
 1151             self.__fileManagerWidget = None
 1152         
 1153         self.filesButton.setChecked(checked)
 1154     
 1155     ##################################################################
 1156     ## Super Menu related methods below
 1157     ##################################################################
 1158     
 1159     def __aboutToShowSuperMenu(self):
 1160         """
 1161         Private slot to populate the Super Menu before showing it.
 1162         """
 1163         self.__superMenu.clear()
 1164         if self.__device:
 1165             hasTime = self.__device.hasTimeCommands()
 1166         else:
 1167             hasTime = False
 1168         
 1169         act = self.__superMenu.addAction(
 1170             self.tr("Show Version"), self.__showDeviceVersion)
 1171         act.setEnabled(self.__connected)
 1172         act = self.__superMenu.addAction(
 1173             self.tr("Show Implementation"), self.__showImplementation)
 1174         act.setEnabled(self.__connected)
 1175         self.__superMenu.addSeparator()
 1176         if hasTime:
 1177             act = self.__superMenu.addAction(
 1178                 self.tr("Synchronize Time"), self.__synchronizeTime)
 1179             act.setEnabled(self.__connected)
 1180             act = self.__superMenu.addAction(
 1181                 self.tr("Show Device Time"), self.__showDeviceTime)
 1182             act.setEnabled(self.__connected)
 1183         self.__superMenu.addAction(
 1184             self.tr("Show Local Time"), self.__showLocalTime)
 1185         if hasTime:
 1186             act = self.__superMenu.addAction(
 1187                 self.tr("Show Time"), self.__showLocalAndDeviceTime)
 1188             act.setEnabled(self.__connected)
 1189         self.__superMenu.addSeparator()
 1190         if not Globals.isWindowsPlatform():
 1191             available = self.__mpyCrossAvailable()
 1192             act = self.__superMenu.addAction(
 1193                 self.tr("Compile Python File"), self.__compileFile2Mpy)
 1194             act.setEnabled(available)
 1195             act = self.__superMenu.addAction(
 1196                 self.tr("Compile Current Editor"), self.__compileEditor2Mpy)
 1197             aw = e5App().getObject("ViewManager").activeWindow()
 1198             act.setEnabled(available and bool(aw))
 1199             self.__superMenu.addSeparator()
 1200         if self.__device:
 1201             self.__device.addDeviceMenuEntries(self.__superMenu)
 1202             self.__superMenu.addSeparator()
 1203             act = self.__superMenu.addAction(
 1204                 self.tr("Download Firmware"), self.__downloadFirmware)
 1205             act.setEnabled(self.__device.hasFirmwareUrl())
 1206             self.__superMenu.addSeparator()
 1207             act = self.__superMenu.addAction(
 1208                 self.tr("Show Documentation"), self.__showDocumentation)
 1209             act.setEnabled(self.__device.hasDocumentationUrl())
 1210         self.__superMenu.addSeparator()
 1211         self.__superMenu.addAction(self.tr("Ignored Serial Devices"),
 1212                                    self.__manageIgnored)
 1213         self.__superMenu.addSeparator()
 1214         self.__superMenu.addAction(self.tr("Configure"), self.__configure)
 1215     
 1216     @pyqtSlot()
 1217     def __showDeviceVersion(self):
 1218         """
 1219         Private slot to show some version info about MicroPython of the device.
 1220         """
 1221         try:
 1222             versionInfo = self.__interface.version()
 1223             if versionInfo:
 1224                 msg = self.tr(
 1225                     "<h3>Device Version Information</h3>"
 1226                 )
 1227                 msg += "<table>"
 1228                 for key, value in versionInfo.items():
 1229                     msg += "<tr><td><b>{0}</b></td><td>{1}</td></tr>".format(
 1230                         key.capitalize(), value)
 1231                 msg += "</table>"
 1232             else:
 1233                 msg = self.tr("No version information available.")
 1234             
 1235             E5MessageBox.information(
 1236                 self,
 1237                 self.tr("Device Version Information"),
 1238                 msg)
 1239         except Exception as exc:
 1240             self.__showError("version()", str(exc))
 1241     
 1242     @pyqtSlot()
 1243     def __showImplementation(self):
 1244         """
 1245         Private slot to show some implementation related information.
 1246         """
 1247         try:
 1248             impInfo = self.__interface.getImplementation()
 1249             if impInfo["name"] == "micropython":
 1250                 name = "MicroPython"
 1251             elif impInfo["name"] == "circuitpython":
 1252                 name = "CircuitPython"
 1253             elif impInfo["name"] == "unknown":
 1254                 name = self.tr("unknown")
 1255             else:
 1256                 name = impInfo["name"]
 1257             if impInfo["version"] == "unknown":
 1258                 version = self.tr("unknown")
 1259             else:
 1260                 version = impInfo["version"]
 1261             
 1262             E5MessageBox.information(
 1263                 self,
 1264                 self.tr("Device Implementation Information"),
 1265                 self.tr(
 1266                     "<h3>Device Implementation Information</h3>"
 1267                     "<p>This device contains <b>{0} {1}</b>.</p>"
 1268                 ).format(name, version)
 1269             )
 1270         except Exception as exc:
 1271             self.__showError("getImplementation()", str(exc))
 1272     
 1273     @pyqtSlot()
 1274     def __synchronizeTime(self, quiet=False):
 1275         """
 1276         Private slot to set the time of the connected device to the local
 1277         computer's time.
 1278         
 1279         @param quiet flag indicating to not show a message
 1280         @type bool
 1281         """
 1282         try:
 1283             self.__interface.syncTime()
 1284             
 1285             if not quiet:
 1286                 E5MessageBox.information(
 1287                     self,
 1288                     self.tr("Synchronize Time"),
 1289                     self.tr("<p>The time of the connected device was"
 1290                             " synchronized with the local time.</p>") +
 1291                     self.__getDeviceTime()
 1292                 )
 1293         except Exception as exc:
 1294             self.__showError("syncTime()", str(exc))
 1295     
 1296     def __getDeviceTime(self):
 1297         """
 1298         Private method to get a string containing the date and time of the
 1299         connected device.
 1300         
 1301         @return date and time of the connected device
 1302         @rtype str
 1303         """
 1304         try:
 1305             dateTimeString = self.__interface.getTime()
 1306             try:
 1307                 date, time = dateTimeString.strip().split(None, 1)
 1308                 return self.tr(
 1309                     "<h3>Device Date and Time</h3>"
 1310                     "<table>"
 1311                     "<tr><td><b>Date</b></td><td>{0}</td></tr>"
 1312                     "<tr><td><b>Time</b></td><td>{1}</td></tr>"
 1313                     "</table>"
 1314                 ).format(date, time)
 1315             except ValueError:
 1316                 return self.tr(
 1317                     "<h3>Device Date and Time</h3>"
 1318                     "<p>{0}</p>"
 1319                 ).format(dateTimeString.strip())
 1320         except Exception as exc:
 1321             self.__showError("getTime()", str(exc))
 1322             return ""
 1323     
 1324     @pyqtSlot()
 1325     def __showDeviceTime(self):
 1326         """
 1327         Private slot to show the date and time of the connected device.
 1328         """
 1329         msg = self.__getDeviceTime()
 1330         E5MessageBox.information(
 1331             self,
 1332             self.tr("Device Date and Time"),
 1333             msg)
 1334     
 1335     @pyqtSlot()
 1336     def __showLocalTime(self):
 1337         """
 1338         Private slot to show the local date and time.
 1339         """
 1340         localdatetime = time.localtime()
 1341         localdate = time.strftime('%Y-%m-%d', localdatetime)
 1342         localtime = time.strftime('%H:%M:%S', localdatetime)
 1343         E5MessageBox.information(
 1344             self,
 1345             self.tr("Local Date and Time"),
 1346             self.tr("<h3>Local Date and Time</h3>"
 1347                     "<table>"
 1348                     "<tr><td><b>Date</b></td><td>{0}</td></tr>"
 1349                     "<tr><td><b>Time</b></td><td>{1}</td></tr>"
 1350                     "</table>"
 1351                     ).format(localdate, localtime)
 1352         )
 1353     
 1354     @pyqtSlot()
 1355     def __showLocalAndDeviceTime(self):
 1356         """
 1357         Private slot to show the local and device time side-by-side.
 1358         """
 1359         localdatetime = time.localtime()
 1360         localdate = time.strftime('%Y-%m-%d', localdatetime)
 1361         localtime = time.strftime('%H:%M:%S', localdatetime)
 1362         
 1363         try:
 1364             deviceDateTimeString = self.__interface.getTime()
 1365             try:
 1366                 devicedate, devicetime = (
 1367                     deviceDateTimeString.strip().split(None, 1)
 1368                 )
 1369                 E5MessageBox.information(
 1370                     self,
 1371                     self.tr("Date and Time"),
 1372                     self.tr("<table>"
 1373                             "<tr><th></th><th>Local Date and Time</th>"
 1374                             "<th>Device Date and Time</th></tr>"
 1375                             "<tr><td><b>Date</b></td>"
 1376                             "<td align='center'>{0}</td>"
 1377                             "<td align='center'>{2}</td></tr>"
 1378                             "<tr><td><b>Time</b></td>"
 1379                             "<td align='center'>{1}</td>"
 1380                             "<td align='center'>{3}</td></tr>"
 1381                             "</table>"
 1382                             ).format(localdate, localtime,
 1383                                      devicedate, devicetime)
 1384                 )
 1385             except ValueError:
 1386                 E5MessageBox.information(
 1387                     self,
 1388                     self.tr("Date and Time"),
 1389                     self.tr("<table>"
 1390                             "<tr><th>Local Date and Time</th>"
 1391                             "<th>Device Date and Time</th></tr>"
 1392                             "<tr><td align='center'>{0} {1}</td>"
 1393                             "<td align='center'>{2}</td></tr>"
 1394                             "</table>"
 1395                             ).format(localdate, localtime,
 1396                                      deviceDateTimeString.strip())
 1397                 )
 1398         except Exception as exc:
 1399             self.__showError("getTime()", str(exc))
 1400     
 1401     def __showError(self, method, error):
 1402         """
 1403         Private method to show some error message.
 1404         
 1405         @param method name of the method the error occured in
 1406         @type str
 1407         @param error error message
 1408         @type str
 1409         """
 1410         E5MessageBox.warning(
 1411             self,
 1412             self.tr("Error handling device"),
 1413             self.tr("<p>There was an error communicating with the connected"
 1414                     " device.</p><p>Method: {0}</p><p>Message: {1}</p>")
 1415             .format(method, error))
 1416     
 1417     def __mpyCrossAvailable(self):
 1418         """
 1419         Private method to check the availability of mpy-cross.
 1420         
 1421         @return flag indicating the availability of mpy-cross
 1422         @rtype bool
 1423         """
 1424         available = False
 1425         program = Preferences.getMicroPython("MpyCrossCompiler")
 1426         if not program:
 1427             program = "mpy-cross"
 1428             if Utilities.isinpath(program):
 1429                 available = True
 1430         else:
 1431             if Utilities.isExecutable(program):
 1432                 available = True
 1433         
 1434         return available
 1435     
 1436     def __crossCompile(self, pythonFile="", title=""):
 1437         """
 1438         Private method to cross compile a Python file to a .mpy file.
 1439         
 1440         @param pythonFile name of the Python file to be compiled
 1441         @type str
 1442         @param title title for the various dialogs
 1443         @type str
 1444         """
 1445         program = Preferences.getMicroPython("MpyCrossCompiler")
 1446         if not program:
 1447             program = "mpy-cross"
 1448             if not Utilities.isinpath(program):
 1449                 E5MessageBox.critical(
 1450                     self,
 1451                     title,
 1452                     self.tr("""The MicroPython cross compiler"""
 1453                             """ <b>mpy-cross</b> cannot be found. Ensure it"""
 1454                             """ is in the search path or configure it on"""
 1455                             """ the MicroPython configuration page."""))
 1456                 return
 1457         
 1458         if not pythonFile:
 1459             defaultDirectory = ""
 1460             aw = e5App().getObject("ViewManager").activeWindow()
 1461             if aw:
 1462                 fn = aw.getFileName()
 1463                 if fn:
 1464                     defaultDirectory = os.path.dirname(fn)
 1465             if not defaultDirectory:
 1466                 defaultDirectory = Preferences.getMultiProject("Workspace")
 1467             pythonFile = E5FileDialog.getOpenFileName(
 1468                 self,
 1469                 title,
 1470                 defaultDirectory,
 1471                 self.tr("Python Files (*.py);;All Files (*)"))
 1472             if not pythonFile:
 1473                 # user cancelled
 1474                 return
 1475         
 1476         if not os.path.exists(pythonFile):
 1477             E5MessageBox.critical(
 1478                 self,
 1479                 title,
 1480                 self.tr("""The Python file <b>{0}</b> does not exist."""
 1481                         """ Aborting...""").format(pythonFile))
 1482             return
 1483         
 1484         compileArgs = [
 1485             pythonFile,
 1486         ]
 1487         dlg = E5ProcessDialog(self.tr("'mpy-cross' Output"), title)
 1488         res = dlg.startProcess(program, compileArgs)
 1489         if res:
 1490             dlg.exec_()
 1491     
 1492     @pyqtSlot()
 1493     def __compileFile2Mpy(self):
 1494         """
 1495         Private slot to cross compile a Python file (*.py) to a .mpy file.
 1496         """
 1497         self.__crossCompile(title=self.tr("Compile Python File"))
 1498     
 1499     @pyqtSlot()
 1500     def __compileEditor2Mpy(self):
 1501         """
 1502         Private slot to cross compile the current editor to a .mpy file.
 1503         """
 1504         aw = e5App().getObject("ViewManager").activeWindow()
 1505         if not aw.checkDirty():
 1506             # editor still has unsaved changes, abort...
 1507             return
 1508         if not aw.isPyFile():
 1509             # no Python file
 1510             E5MessageBox.critical(
 1511                 self,
 1512                 self.tr("Compile Current Editor"),
 1513                 self.tr("""The current editor does not contain a Python"""
 1514                         """ file. Aborting..."""))
 1515             return
 1516         
 1517         self.__crossCompile(
 1518             pythonFile=aw.getFileName(),
 1519             title=self.tr("Compile Current Editor")
 1520         )
 1521     
 1522     @pyqtSlot()
 1523     def __showDocumentation(self):
 1524         """
 1525         Private slot to open the documentation URL for the selected device.
 1526         """
 1527         if self.__device is None or not self.__device.hasDocumentationUrl():
 1528             # abort silently
 1529             return
 1530         
 1531         url = self.__device.getDocumentationUrl()
 1532         e5App().getObject("UserInterface").launchHelpViewer(url)
 1533     
 1534     @pyqtSlot()
 1535     def __downloadFirmware(self):
 1536         """
 1537         Private slot to open the firmware download page.
 1538         """
 1539         if self.__device is None or not self.__device.hasFirmwareUrl():
 1540             # abort silently
 1541             return
 1542         
 1543         url = self.__device.getFirmwareUrl()
 1544         e5App().getObject("UserInterface").launchHelpViewer(url)
 1545     
 1546     @pyqtSlot()
 1547     def __manageIgnored(self):
 1548         """
 1549         Private slot to manage the list of ignored serial devices.
 1550         """
 1551         from .IgnoredDevicesDialog import IgnoredDevicesDialog
 1552         
 1553         dlg = IgnoredDevicesDialog(
 1554             Preferences.getMicroPython("IgnoredUnknownDevices"),
 1555             self)
 1556         if dlg.exec_() == QDialog.Accepted:
 1557             ignoredDevices = dlg.getDevices()
 1558             Preferences.setMicroPython("IgnoredUnknownDevices",
 1559                                        ignoredDevices)
 1560     
 1561     @pyqtSlot()
 1562     def __configure(self):
 1563         """
 1564         Private slot to open the MicroPython configuration page.
 1565         """
 1566         e5App().getObject("UserInterface").showPreferences("microPythonPage")