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 ( |
14 from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QPoint, QEvent |
15 pyqtSlot, pyqtSignal, Qt, QPoint, QEvent, QIODevice, QTimer |
15 from PyQt5.QtGui import QColor, QKeySequence, QTextCursor, QBrush |
16 ) |
|
17 from PyQt5.QtGui import ( |
|
18 QColor, QKeySequence, QTextCursor, QBrush |
|
19 ) |
|
20 from PyQt5.QtWidgets import ( |
16 from PyQt5.QtWidgets import ( |
21 QWidget, QMenu, QApplication, QHBoxLayout, QSpacerItem, QSizePolicy |
17 QWidget, QMenu, QApplication, QHBoxLayout, QSpacerItem, QSizePolicy |
22 ) |
18 ) |
23 try: |
|
24 from PyQt5.QtSerialPort import QSerialPort |
|
25 HAS_QTSERIALPORT = True |
|
26 except ImportError: |
|
27 HAS_QTSERIALPORT = False |
|
28 |
19 |
29 from E5Gui.E5ZoomWidget import E5ZoomWidget |
20 from E5Gui.E5ZoomWidget import E5ZoomWidget |
30 from E5Gui import E5MessageBox, E5FileDialog |
21 from E5Gui import E5MessageBox, E5FileDialog |
31 from E5Gui.E5Application import e5App |
22 from E5Gui.E5Application import e5App |
32 |
23 |
367 """ of MicroPython (or CircuitPython) flashed onto""" |
381 """ of MicroPython (or CircuitPython) flashed onto""" |
368 """ it before anything will work.\n\nFinally press""" |
382 """ it before anything will work.\n\nFinally press""" |
369 """ the device's reset button and wait a few seconds""" |
383 """ the device's reset button and wait a few seconds""" |
370 """ before trying again.""")) |
384 """ before trying again.""")) |
371 |
385 |
372 @pyqtSlot() |
386 @pyqtSlot(bool) |
373 def on_replButton_clicked(self): |
387 def on_replButton_clicked(self, checked): |
374 """ |
388 """ |
375 Private slot to connect to the selected device and start a REPL. |
389 Private slot to connect to enable or disable the REPL widget connecting |
|
390 or disconnecting from the device. |
|
391 |
|
392 @param checked state of the button |
|
393 @type bool |
376 """ |
394 """ |
377 if not self.__device: |
395 if not self.__device: |
378 self.__showNoDeviceMessage() |
396 self.__showNoDeviceMessage() |
379 return |
397 return |
380 |
398 |
381 if self.__replRunning: |
399 if checked: |
382 self.dataReceived.disconnect(self.__processData) |
|
383 if not self.__plotterRunning: |
|
384 self.__disconnectSerial() |
|
385 self.__replRunning = False |
|
386 self.__device.setRepl(False) |
|
387 else: |
|
388 ok, reason = self.__device.canStartRepl() |
400 ok, reason = self.__device.canStartRepl() |
389 if not ok: |
401 if not ok: |
390 E5MessageBox.warning( |
402 E5MessageBox.warning( |
391 self, |
403 self, |
392 self.tr("Start REPL"), |
404 self.tr("Start REPL"), |
393 self.tr("""<p>The REPL cannot be started.</p><p>Reason:""" |
405 self.tr("""<p>The REPL cannot be started.</p><p>Reason:""" |
394 """ {0}</p>""").format(reason)) |
406 """ {0}</p>""").format(reason)) |
395 return |
407 return |
396 |
408 |
397 self.replEdit.clear() |
409 self.replEdit.clear() |
398 self.dataReceived.connect(self.__processData) |
410 self.__interface.dataReceived.connect(self.__processData) |
399 |
411 |
400 if not self.__serial: |
412 if not self.__interface.isConnected(): |
401 self.__openSerialLink() |
413 self.__connectToDevice() |
402 if self.__serial: |
414 if self.__device.forceInterrupt(): |
403 if self.__device.forceInterrupt(): |
415 # send a Ctrl-B (exit raw mode) |
404 # send a Ctrl-B (exit raw mode) |
416 self.__interface.write(b'\x02') |
405 self.__serial.write(b'\x02') |
417 # send Ctrl-C (keyboard interrupt) |
406 # send Ctrl-C (keyboard interrupt) |
418 self.__interface.write(b'\x03') |
407 self.__serial.write(b'\x03') |
|
408 |
419 |
409 self.__replRunning = True |
420 self.__replRunning = True |
410 self.__device.setRepl(True) |
421 self.__device.setRepl(True) |
411 self.replEdit.setFocus(Qt.OtherFocusReason) |
422 self.replEdit.setFocus(Qt.OtherFocusReason) |
|
423 else: |
|
424 self.__interface.dataReceived.disconnect(self.__processData) |
|
425 if not self.__plotterRunning and not self.__fileManagerRunning: |
|
426 self.__disconnectFromDevice() |
|
427 self.__replRunning = False |
|
428 self.__device.setRepl(False) |
|
429 self.replButton.setChecked(checked) |
412 |
430 |
413 @pyqtSlot() |
431 @pyqtSlot() |
414 def on_disconnectButton_clicked(self): |
432 def on_disconnectButton_clicked(self): |
415 """ |
433 """ |
416 Private slot to disconnect from the currently connected device. |
434 Private slot to disconnect from the currently connected device. |
417 """ |
435 """ |
418 if self.__replRunning: |
436 self.__disconnectFromDevice() |
419 self.on_replButton_clicked() |
|
420 |
|
421 if self.__plotterRunning: |
|
422 self.on_chartButton_clicked() |
|
423 |
|
424 def __disconnectSerial(self): |
|
425 """ |
|
426 Private slot to disconnect the serial connection. |
|
427 """ |
|
428 self.__closeSerialLink() |
|
429 self.setConnected(False) |
|
430 |
437 |
431 @pyqtSlot() |
438 @pyqtSlot() |
432 def __clear(self): |
439 def __clear(self): |
433 """ |
440 """ |
434 Private slot to clear the REPL pane. |
441 Private slot to clear the REPL pane. |
435 """ |
442 """ |
436 self.replEdit.clear() |
443 self.replEdit.clear() |
437 self.__serial and self.__serial.write(b"\r") |
444 self.__interface.isConnected() and self.__interface.write(b"\r") |
438 |
445 |
439 @pyqtSlot() |
446 @pyqtSlot() |
440 def __paste(self): |
447 def __paste(self): |
441 """ |
448 """ |
442 Private slot to perform a paste operation. |
449 Private slot to perform a paste operation. |
751 return portName |
753 return portName |
752 else: |
754 else: |
753 # return with device path prepended |
755 # return with device path prepended |
754 return "/dev/{0}".format(portName) |
756 return "/dev/{0}".format(portName) |
755 |
757 |
756 def __openSerialLink(self): |
758 def __connectToDevice(self): |
757 """ |
759 """ |
758 Private method to open a serial link to the selected device. |
760 Private method to connect to the selected device. |
759 """ |
761 """ |
760 port = self.__getCurrentPort() |
762 port = self.__getCurrentPort() |
761 self.__serial = QSerialPort() |
763 if self.__interface.connectToDevice(port): |
762 self.__serial.setPortName(port) |
|
763 if self.__serial.open(QIODevice.ReadWrite): |
|
764 self.__serial.setDataTerminalReady(True) |
|
765 # 115.200 baud, 8N1 |
|
766 self.__serial.setBaudRate(115200) |
|
767 self.__serial.setDataBits(QSerialPort.Data8) |
|
768 self.__serial.setParity(QSerialPort.NoParity) |
|
769 self.__serial.setStopBits(QSerialPort.OneStop) |
|
770 self.__serial.readyRead.connect(self.__readSerial) |
|
771 self.setConnected(True) |
764 self.setConnected(True) |
772 else: |
765 else: |
773 E5MessageBox.warning( |
766 E5MessageBox.warning( |
774 self, |
767 self, |
775 self.tr("Serial Device Connect"), |
768 self.tr("Serial Device Connect"), |
776 self.tr("""<p>Cannot connect to device at serial port""" |
769 self.tr("""<p>Cannot connect to device at serial port""" |
777 """ <b>{0}</b>.</p>""").format(port)) |
770 """ <b>{0}</b>.</p>""").format(port)) |
778 self.__serial = None |
771 |
779 |
772 def __disconnectFromDevice(self): |
780 def __closeSerialLink(self): |
773 """ |
781 """ |
774 Private method to disconnect from the device. |
782 Private method to close the open serial connection. |
775 """ |
783 """ |
776 self.__interface.disconnectFromDevice() |
784 if self.__serial: |
777 self.setConnected(False) |
785 self.__serial.close() |
|
786 self.__serial = None |
|
787 |
|
788 @pyqtSlot() |
|
789 def __readSerial(self): |
|
790 """ |
|
791 Private slot to read all available serial data and emit it with the |
|
792 "dataReceived" signal for further processing. |
|
793 """ |
|
794 data = bytes(self.__serial.readAll()) |
|
795 self.dataReceived.emit(data) |
|
796 |
|
797 def execute(self, commandsList): |
|
798 """ |
|
799 Public method to execute a series of commands over a period of time. |
|
800 |
|
801 @param commandsList list of commands to be execute on the device |
|
802 @type list of bytes |
|
803 """ |
|
804 def remainingTask(commands): |
|
805 self.execute(commands) |
|
806 |
|
807 if commandsList: |
|
808 command = commandsList[0] |
|
809 self.__serial.write(command) |
|
810 remainder = commandsList[1:] |
|
811 QTimer.singleShot(2, lambda: remainingTask(remainder)) |
|
812 |
778 |
813 @pyqtSlot() |
779 @pyqtSlot() |
814 def on_runButton_clicked(self): |
780 def on_runButton_clicked(self): |
815 """ |
781 """ |
816 Private slot to execute the script of the active editor on the |
782 Private slot to execute the script of the active editor on the |
881 workspace = self.__device.getWorkspace() |
848 workspace = self.__device.getWorkspace() |
882 aw = e5App().getObject("ViewManager").activeWindow() |
849 aw = e5App().getObject("ViewManager").activeWindow() |
883 if aw: |
850 if aw: |
884 aw.saveFileAs(workspace) |
851 aw.saveFileAs(workspace) |
885 |
852 |
886 @pyqtSlot() |
853 @pyqtSlot(bool) |
887 def on_chartButton_clicked(self): |
854 def on_chartButton_clicked(self, checked): |
888 """ |
855 """ |
889 Private slot to open a chart view to plot data received from the |
856 Private slot to open a chart view to plot data received from the |
890 connected device. |
857 connected device. |
|
858 |
|
859 @param checked state of the button |
|
860 @type bool |
891 """ |
861 """ |
892 if not HAS_QTCHART: |
862 if not HAS_QTCHART: |
893 # QtChart not available => fail silently |
863 # QtChart not available => fail silently |
894 return |
864 return |
895 |
865 |
896 if not self.__device: |
866 if not self.__device: |
897 self.__showNoDeviceMessage() |
867 self.__showNoDeviceMessage() |
898 return |
868 return |
899 |
869 |
900 if self.__plotterRunning: |
870 if checked: |
|
871 ok, reason = self.__device.canStartPlotter() |
|
872 if not ok: |
|
873 E5MessageBox.warning( |
|
874 self, |
|
875 self.tr("Start Chart"), |
|
876 self.tr("""<p>The Chart cannot be started.</p><p>Reason:""" |
|
877 """ {0}</p>""").format(reason)) |
|
878 return |
|
879 |
|
880 self.__chartWidget = MicroPythonGraphWidget(self) |
|
881 self.__interface.dataReceived.connect( |
|
882 self.__chartWidget.processData) |
|
883 self.__chartWidget.dataFlood.connect( |
|
884 self.handleDataFlood) |
|
885 |
|
886 self.__ui.addSideWidget(self.__ui.BottomSide, self.__chartWidget, |
|
887 UI.PixmapCache.getIcon("chart"), |
|
888 self.tr("μPy Chart")) |
|
889 self.__ui.showSideWidget(self.__chartWidget) |
|
890 |
|
891 if not self.__interface.isConnected(): |
|
892 self.__connectToDevice() |
|
893 if self.__device.forceInterrupt(): |
|
894 # send a Ctrl-B (exit raw mode) |
|
895 self.__interface.write(b'\x02') |
|
896 # send Ctrl-C (keyboard interrupt) |
|
897 self.__interface.write(b'\x03') |
|
898 |
|
899 self.__plotterRunning = True |
|
900 self.__device.setPlotter(True) |
|
901 else: |
901 if self.__chartWidget.isDirty(): |
902 if self.__chartWidget.isDirty(): |
902 res = E5MessageBox.okToClearData( |
903 res = E5MessageBox.okToClearData( |
903 self, |
904 self, |
904 self.tr("Unsaved Chart Data"), |
905 self.tr("Unsaved Chart Data"), |
905 self.tr("""The chart contains unsaved data."""), |
906 self.tr("""The chart contains unsaved data."""), |
906 self.__chartWidget.saveData) |
907 self.__chartWidget.saveData) |
907 if not res: |
908 if not res: |
908 # abort |
909 # abort |
909 return |
910 return |
910 |
911 |
911 self.dataReceived.disconnect(self.__chartWidget.processData) |
912 self.__interface.dataReceived.disconnect( |
912 self.__chartWidget.dataFlood.disconnect(self.handleDataFlood) |
913 self.__chartWidget.processData) |
913 |
914 self.__chartWidget.dataFlood.disconnect( |
914 if not self.__replRunning: |
915 self.handleDataFlood) |
915 self.__disconnectSerial() |
916 |
|
917 if not self.__replRunning and not self.__fileManagerRunning: |
|
918 self.__disconnectFromDevice() |
916 |
919 |
917 self.__plotterRunning = False |
920 self.__plotterRunning = False |
918 self.__device.setPlotter(False) |
921 self.__device.setPlotter(False) |
919 self.__ui.removeSideWidget(self.__chartWidget) |
922 self.__ui.removeSideWidget(self.__chartWidget) |
920 else: |
923 |
921 ok, reason = self.__device.canStartPlotter() |
924 self.__chartWidget.deleteLater() |
922 if not ok: |
925 self.__chartWidget = None |
923 E5MessageBox.warning( |
926 |
924 self, |
927 self.chartButton.setChecked(checked) |
925 self.tr("Start Chart"), |
|
926 self.tr("""<p>The Chart cannot be started.</p><p>Reason:""" |
|
927 """ {0}</p>""").format(reason)) |
|
928 return |
|
929 |
|
930 self.__chartWidget = MicroPythonGraphWidget(self) |
|
931 self.dataReceived.connect(self.__chartWidget.processData) |
|
932 self.__chartWidget.dataFlood.connect(self.handleDataFlood) |
|
933 |
|
934 self.__ui.addSideWidget(self.__ui.BottomSide, self.__chartWidget, |
|
935 UI.PixmapCache.getIcon("chart"), |
|
936 self.tr("μPy Chart")) |
|
937 self.__ui.showSideWidget(self.__chartWidget) |
|
938 |
|
939 if not self.__serial: |
|
940 self.__openSerialLink() |
|
941 if self.__serial: |
|
942 if self.__device.forceInterrupt(): |
|
943 # send a Ctrl-B (exit raw mode) |
|
944 self.__serial.write(b'\x02') |
|
945 # send Ctrl-C (keyboard interrupt) |
|
946 self.__serial.write(b'\x03') |
|
947 |
|
948 self.__plotterRunning = True |
|
949 self.__device.setPlotter(True) |
|
950 |
928 |
951 @pyqtSlot() |
929 @pyqtSlot() |
952 def handleDataFlood(self): |
930 def handleDataFlood(self): |
953 """ |
931 """ |
954 Public slot handling a data flood from the device. |
932 Public slot handling a data flood from the device. |
955 """ |
933 """ |
956 self.on_disconnectButton_clicked() |
934 self.on_disconnectButton_clicked() |
957 self.__device.handleDataFlood() |
935 self.__device.handleDataFlood() |
958 |
936 |
959 @pyqtSlot() |
937 @pyqtSlot(bool) |
960 def on_filesButton_clicked(self): |
938 def on_filesButton_clicked(self, checked): |
961 """ |
939 """ |
962 Private slot to open a file manager window to the connected device. |
940 Private slot to open a file manager window to the connected device. |
|
941 |
|
942 @param checked state of the button |
|
943 @type bool |
963 """ |
944 """ |
964 if not self.__device: |
945 if not self.__device: |
965 self.__showNoDeviceMessage() |
946 self.__showNoDeviceMessage() |
966 return |
947 return |
967 |
948 |
968 if self.__fileManagerRunning: |
949 if checked: |
969 self.__fileManagerWidget.stop() |
|
970 self.__ui.removeSideWidget(self.__fileManagerWidget) |
|
971 |
|
972 self.__device.setFileManager(False) |
|
973 self.__fileManagerRunning = False |
|
974 else: |
|
975 ok, reason = self.__device.canStartFileManager() |
950 ok, reason = self.__device.canStartFileManager() |
976 if not ok: |
951 if not ok: |
977 E5MessageBox.warning( |
952 E5MessageBox.warning( |
978 self, |
953 self, |
979 self.tr("Start File Manager"), |
954 self.tr("Start File Manager"), |
980 self.tr("""<p>The File Manager cannot be started.</p>""" |
955 self.tr("""<p>The File Manager cannot be started.</p>""" |
981 """<p>Reason: {0}</p>""").format(reason)) |
956 """<p>Reason: {0}</p>""").format(reason)) |
982 return |
957 return |
983 |
958 |
984 port = self.__getCurrentPort() |
959 if not self.__interface.isConnected(): |
985 self.__fileManagerWidget = MicroPythonFileManagerWidget(port, self) |
960 self.__connectToDevice() |
|
961 self.__fileManagerWidget = MicroPythonFileManagerWidget( |
|
962 self.__interface, self) |
986 |
963 |
987 self.__ui.addSideWidget(self.__ui.BottomSide, |
964 self.__ui.addSideWidget(self.__ui.BottomSide, |
988 self.__fileManagerWidget, |
965 self.__fileManagerWidget, |
989 UI.PixmapCache.getIcon("filemanager"), |
966 UI.PixmapCache.getIcon("filemanager"), |
990 self.tr("μPy Files")) |
967 self.tr("μPy Files")) |