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