9 |
9 |
10 from __future__ import unicode_literals |
10 from __future__ import unicode_literals |
11 |
11 |
12 import re |
12 import re |
13 |
13 |
14 from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QPoint, QEvent, QIODevice |
14 from PyQt5.QtCore import ( |
|
15 pyqtSlot, pyqtSignal, Qt, QPoint, QEvent, QIODevice, QTimer |
|
16 ) |
15 from PyQt5.QtGui import QColor, QKeySequence, QTextCursor |
17 from PyQt5.QtGui import QColor, QKeySequence, QTextCursor |
16 from PyQt5.QtWidgets import ( |
18 from PyQt5.QtWidgets import ( |
17 QWidget, QMenu, QApplication, QHBoxLayout, QSpacerItem, QSizePolicy) |
19 QWidget, QMenu, QApplication, QHBoxLayout, QSpacerItem, QSizePolicy) |
18 try: |
20 try: |
19 from PyQt5.QtSerialPort import QSerialPort |
21 from PyQt5.QtSerialPort import QSerialPort |
20 HAS_QTSERIALPORT = True |
22 HAS_QTSERIALPORT = True |
21 except ImportError: |
23 except ImportError: |
22 HAS_QTSERIALPORT = False |
24 HAS_QTSERIALPORT = False |
|
25 try: |
|
26 from PyQt5.QtChart import QChart # __IGNORE_WARNING__ |
|
27 HAS_QTCHART = True |
|
28 except ImportError: |
|
29 HAS_QTCHART = False |
23 |
30 |
24 from E5Gui.E5ZoomWidget import E5ZoomWidget |
31 from E5Gui.E5ZoomWidget import E5ZoomWidget |
25 from E5Gui import E5MessageBox |
32 from E5Gui import E5MessageBox, E5FileDialog |
|
33 from E5Gui.E5Application import e5App |
26 |
34 |
27 from .Ui_MicroPythonReplWidget import Ui_MicroPythonReplWidget |
35 from .Ui_MicroPythonReplWidget import Ui_MicroPythonReplWidget |
28 |
36 |
29 from . import MicroPythonDevices |
37 from . import MicroPythonDevices |
30 |
38 |
57 super(MicroPythonReplWidget, self).__init__(parent) |
65 super(MicroPythonReplWidget, self).__init__(parent) |
58 self.setupUi(self) |
66 self.setupUi(self) |
59 |
67 |
60 self.deviceIconLabel.setPixmap(MicroPythonDevices.getDeviceIcon( |
68 self.deviceIconLabel.setPixmap(MicroPythonDevices.getDeviceIcon( |
61 "", False)) |
69 "", False)) |
|
70 |
|
71 self.openButton.setIcon(UI.PixmapCache.getIcon("open")) |
|
72 self.saveButton.setIcon(UI.PixmapCache.getIcon("fileSaveAs")) |
|
73 |
62 self.checkButton.setIcon(UI.PixmapCache.getIcon("question")) |
74 self.checkButton.setIcon(UI.PixmapCache.getIcon("question")) |
63 self.runButton.setIcon(UI.PixmapCache.getIcon("start")) |
75 self.runButton.setIcon(UI.PixmapCache.getIcon("start")) |
64 self.replButton.setIcon(UI.PixmapCache.getIcon("terminal")) |
76 self.replButton.setIcon(UI.PixmapCache.getIcon("terminal")) |
65 self.filesButton.setIcon(UI.PixmapCache.getIcon("filemanager")) |
77 self.filesButton.setIcon(UI.PixmapCache.getIcon("filemanager")) |
66 self.chartButton.setIcon(UI.PixmapCache.getIcon("chart")) |
78 self.chartButton.setIcon(UI.PixmapCache.getIcon("chart")) |
169 def setActionButtons(self, **kwargs): |
181 def setActionButtons(self, **kwargs): |
170 """ |
182 """ |
171 Public method to set the enabled state of the various action buttons. |
183 Public method to set the enabled state of the various action buttons. |
172 |
184 |
173 @param kwargs keyword arguments containg the enabled states (keys are |
185 @param kwargs keyword arguments containg the enabled states (keys are |
174 'run', 'repl', 'files', 'chart' |
186 'run', 'repl', 'files', 'chart', 'open', 'save' |
175 @type dict |
187 @type dict |
176 """ |
188 """ |
|
189 if "open" in kwargs: |
|
190 self.openButton.setEnabled(kwargs["open"]) |
|
191 if "save" in kwargs: |
|
192 self.saveButton.setEnabled(kwargs["save"]) |
177 if "run" in kwargs: |
193 if "run" in kwargs: |
178 self.runButton.setEnabled(kwargs["run"]) |
194 self.runButton.setEnabled(kwargs["run"]) |
179 if "repl" in kwargs: |
195 if "repl" in kwargs: |
180 self.replButton.setEnabled(kwargs["repl"]) |
196 self.replButton.setEnabled(kwargs["repl"]) |
181 if "files" in kwargs: |
197 if "files" in kwargs: |
216 |
232 |
217 self.deviceTypeComboBox.setEnabled(not connected) |
233 self.deviceTypeComboBox.setEnabled(not connected) |
218 |
234 |
219 self.disconnectButton.setEnabled(connected) |
235 self.disconnectButton.setEnabled(connected) |
220 |
236 |
|
237 def __showNoDeviceMessage(self): |
|
238 """ |
|
239 Private method to show a message dialog indicating a missing device. |
|
240 """ |
|
241 E5MessageBox.critical( |
|
242 self, |
|
243 self.tr("No device attached"), |
|
244 self.tr("""Please ensure the device is plugged into your""" |
|
245 """ computer and selected.\n\nIt must have a version""" |
|
246 """ of MicroPython (or CircuitPython) flashed onto""" |
|
247 """ it before anything will work.\n\nFinally press""" |
|
248 """ the device's reset button and wait a few seconds""" |
|
249 """ before trying again.""")) |
|
250 |
221 @pyqtSlot() |
251 @pyqtSlot() |
222 def on_replButton_clicked(self): |
252 def on_replButton_clicked(self): |
223 """ |
253 """ |
224 Private slot to connect to the selected device and start a REPL. |
254 Private slot to connect to the selected device and start a REPL. |
225 """ |
255 """ |
226 if not self.__device: |
256 if not self.__device: |
227 E5MessageBox.critical( |
257 self.__showNoDeviceMessage() |
228 self, |
|
229 self.tr("No device attached"), |
|
230 self.tr("""Please ensure the device is plugged inti your""" |
|
231 """ computer.\n\nIt must have a version of""" |
|
232 """ MicroPython (or CircuitPython) flashed onto it""" |
|
233 """ before the REPL will work.\n\nFinally press the""" |
|
234 """ device's reset button and wait a few seconds""" |
|
235 """ before trying again.""")) |
|
236 return |
258 return |
237 |
259 |
238 if self.__replRunning: |
260 if self.__replRunning: |
239 self.dataReceived.disconnect(self.__processData) |
261 self.dataReceived.disconnect(self.__processData) |
240 self.on_disconnectButton_clicked() |
262 self.on_disconnectButton_clicked() |
258 if self.__device.forceInterrupt(): |
280 if self.__device.forceInterrupt(): |
259 # send a Ctrl-B (exit raw mode) |
281 # send a Ctrl-B (exit raw mode) |
260 self.__serial.write(b'\x02') |
282 self.__serial.write(b'\x02') |
261 # send Ctrl-C (keyboard interrupt) |
283 # send Ctrl-C (keyboard interrupt) |
262 self.__serial.write(b'\x03') |
284 self.__serial.write(b'\x03') |
|
285 |
|
286 self.__replRunning = True |
|
287 self.__device.setRepl(True) |
263 |
288 |
264 @pyqtSlot() |
289 @pyqtSlot() |
265 def on_disconnectButton_clicked(self): |
290 def on_disconnectButton_clicked(self): |
266 """ |
291 """ |
267 Private slot to disconnect from the currently connected device. |
292 Private slot to disconnect from the currently connected device. |
366 tc = self.replEdit.textCursor() |
390 tc = self.replEdit.textCursor() |
367 # the text cursor must be on the last line |
391 # the text cursor must be on the last line |
368 while tc.movePosition(QTextCursor.Down): |
392 while tc.movePosition(QTextCursor.Down): |
369 pass |
393 pass |
370 |
394 |
371 index = 1 |
395 index = 0 |
372 while index < len(data): |
396 while index < len(data): |
373 if data[index] == 8: # \b |
397 if data[index] == 8: # \b |
374 tc.movePosition(QTextCursor.Left) |
398 tc.movePosition(QTextCursor.Left) |
375 self.replEdit.setTextCursor(tc) |
399 self.replEdit.setTextCursor(tc) |
376 elif data[index] == 13: # \r |
400 elif data[index] == 13: # \r |
378 elif (len(data) > index + 1 and |
402 elif (len(data) > index + 1 and |
379 data[index] == 27 and |
403 data[index] == 27 and |
380 data[index + 1] == 91): |
404 data[index + 1] == 91): |
381 # VT100 cursor command detected: <Esc>[ |
405 # VT100 cursor command detected: <Esc>[ |
382 index += 2 # move index to after the [ |
406 index += 2 # move index to after the [ |
383 match = self.__vt100Re.search(data[index:].decaode("utf-8")) |
407 match = self.__vt100Re.search(data[index:].decode("utf-8")) |
384 if match: |
408 if match: |
385 # move to last position in control sequence |
409 # move to last position in control sequence |
386 # ++ will be done at end of loop |
410 # ++ will be done at end of loop |
387 index += match.end() - 1 |
411 index += match.end() - 1 |
388 |
412 |
492 Private slot to read all available serial data and emit it with the |
516 Private slot to read all available serial data and emit it with the |
493 "dataReceived" signal for further processing. |
517 "dataReceived" signal for further processing. |
494 """ |
518 """ |
495 data = bytes(self.__serial.readAll()) |
519 data = bytes(self.__serial.readAll()) |
496 self.dataReceived.emit(data) |
520 self.dataReceived.emit(data) |
|
521 |
|
522 def execute(self, commandsList): |
|
523 """ |
|
524 Public method to execute a series of commands over a period of time. |
|
525 |
|
526 @param commandsList list of commands to be execute on the device |
|
527 @type list of bytes |
|
528 """ |
|
529 if commandsList: |
|
530 command = commandsList[0] |
|
531 self.__serial.write(command) |
|
532 remainder = commandsList[1:] |
|
533 remainingTask = lambda commands=remainder: self.execute(commands) |
|
534 QTimer.singleShot(2, remainingTask) |
|
535 |
|
536 @pyqtSlot() |
|
537 def on_runButton_clicked(self): |
|
538 """ |
|
539 Private slot to execute the script of the active editor on the |
|
540 selected device. |
|
541 """ |
|
542 if not self.__device: |
|
543 self.__showNoDeviceMessage() |
|
544 return |
|
545 |
|
546 aw = e5App().getObject("ViewManager").activeWindow() |
|
547 if aw is None: |
|
548 E5MessageBox.critical( |
|
549 self, |
|
550 self.tr("Run Script"), |
|
551 self.tr("""There is no editor open. Abort...""")) |
|
552 return |
|
553 |
|
554 script = aw.text() |
|
555 if not script: |
|
556 E5MessageBox.critical( |
|
557 self, |
|
558 self.tr("Run Script"), |
|
559 self.tr("""The current editor does not contain a script.""" |
|
560 """ Abort...""")) |
|
561 return |
|
562 |
|
563 ok, reason = self.__device.canRunScript() |
|
564 if not ok: |
|
565 E5MessageBox.warning( |
|
566 self, |
|
567 self.tr("Run Script"), |
|
568 self.tr("""<p>Cannot run script.</p><p>Reason:""" |
|
569 """ {0}</p>""").format(reason)) |
|
570 return |
|
571 |
|
572 if not self.__replRunning: |
|
573 self.on_replButton_clicked() |
|
574 if self.__replRunning: |
|
575 self.__device.runScript(script) |
|
576 |
|
577 @pyqtSlot() |
|
578 def on_openButton_clicked(self): |
|
579 """ |
|
580 Private slot to open a file of the connected device. |
|
581 """ |
|
582 if not self.__device: |
|
583 self.__showNoDeviceMessage() |
|
584 return |
|
585 |
|
586 workspace = self.__device.getWorkspace() |
|
587 fileName = E5FileDialog.getOpenFileName( |
|
588 self, |
|
589 self.tr("Open Python File"), |
|
590 workspace, |
|
591 self.tr("Python3 Files (*.py)")) |
|
592 if fileName: |
|
593 e5App().getObject("ViewManager").openSourceFile(fileName) |
|
594 |
|
595 @pyqtSlot() |
|
596 def on_saveButton_clicked(self): |
|
597 """ |
|
598 Private slot to save the current editor to the connected device. |
|
599 """ |
|
600 if not self.__device: |
|
601 self.__showNoDeviceMessage() |
|
602 return |
|
603 |
|
604 workspace = self.__device.getWorkspace() |
|
605 aw = e5App().getObject("ViewManager").activeWindow() |
|
606 aw.saveFileAs(workspace) |