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, Qt, QPoint, QEvent |
14 from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QPoint, QEvent, QIODevice |
15 from PyQt5.QtGui import QColor, QKeySequence, QTextCursor |
15 from PyQt5.QtGui import QColor, QKeySequence, QTextCursor |
16 from PyQt5.QtWidgets import ( |
16 from PyQt5.QtWidgets import ( |
17 QWidget, QMenu, QApplication, QHBoxLayout, QSpacerItem, QSizePolicy) |
17 QWidget, QMenu, QApplication, QHBoxLayout, QSpacerItem, QSizePolicy) |
18 try: |
18 try: |
19 from PyQt5.QtSerialPort import QSerialPortInfo # __IGNORE_WARNING__ |
19 from PyQt5.QtSerialPort import QSerialPort |
20 HAS_QTSERIALPORT = True |
20 HAS_QTSERIALPORT = True |
21 except ImportError: |
21 except ImportError: |
22 HAS_QTSERIALPORT = False |
22 HAS_QTSERIALPORT = False |
23 |
23 |
24 from E5Gui.E5ZoomWidget import E5ZoomWidget |
24 from E5Gui.E5ZoomWidget import E5ZoomWidget |
|
25 from E5Gui import E5MessageBox |
25 |
26 |
26 from .Ui_MicroPythonReplWidget import Ui_MicroPythonReplWidget |
27 from .Ui_MicroPythonReplWidget import Ui_MicroPythonReplWidget |
27 |
28 |
28 from . import MicroPythonDevices |
29 from . import MicroPythonDevices |
29 |
30 |
32 |
33 |
33 |
34 |
34 class MicroPythonReplWidget(QWidget, Ui_MicroPythonReplWidget): |
35 class MicroPythonReplWidget(QWidget, Ui_MicroPythonReplWidget): |
35 """ |
36 """ |
36 Class implementing the MicroPython REPL widget. |
37 Class implementing the MicroPython REPL widget. |
|
38 |
|
39 @signal dataReceived(data) emitted to send data received via the serial |
|
40 connection for further processing |
37 """ |
41 """ |
38 ZoomMin = -10 |
42 ZoomMin = -10 |
39 ZoomMax = 20 |
43 ZoomMax = 20 |
|
44 |
|
45 DeviceTypeRole = Qt.UserRole |
|
46 DevicePortRole = Qt.UserRole + 1 |
|
47 |
|
48 dataReceived = pyqtSignal(bytes) |
40 |
49 |
41 def __init__(self, parent=None): |
50 def __init__(self, parent=None): |
42 """ |
51 """ |
43 Constructor |
52 Constructor |
44 |
53 |
78 |
87 |
79 self.__serial = None |
88 self.__serial = None |
80 self.__device = None |
89 self.__device = None |
81 self.setConnected(False) |
90 self.setConnected(False) |
82 |
91 |
|
92 self.__replRunning = False |
|
93 self.__plotterRunning = False |
|
94 self.__fileManagerRunning = False |
|
95 |
83 if not HAS_QTSERIALPORT: |
96 if not HAS_QTSERIALPORT: |
84 self.replEdit.setHtml(self.tr( |
97 self.replEdit.setHtml(self.tr( |
85 "<h3>The QtSerialPort package is not available.<br/>" |
98 "<h3>The QtSerialPort package is not available.<br/>" |
86 "MicroPython support is deactivated.</h3>")) |
99 "MicroPython support is deactivated.</h3>")) |
87 self.setEnabled(False) |
100 self.setEnabled(False) |
111 self.deviceTypeComboBox.addItem("", "") |
124 self.deviceTypeComboBox.addItem("", "") |
112 devices = MicroPythonDevices.getFoundDevices() |
125 devices = MicroPythonDevices.getFoundDevices() |
113 if devices: |
126 if devices: |
114 self.deviceInfoLabel.setText( |
127 self.deviceInfoLabel.setText( |
115 self.tr("%n supported device(s) detected.", n=len(devices))) |
128 self.tr("%n supported device(s) detected.", n=len(devices))) |
|
129 |
|
130 index = 0 |
116 for device in sorted(devices): |
131 for device in sorted(devices): |
|
132 index += 1 |
117 self.deviceTypeComboBox.addItem( |
133 self.deviceTypeComboBox.addItem( |
118 self.tr("{0} at {1}".format(device[1], device[2])), |
134 self.tr("{0} at {1}".format(device[1], device[2]))) |
119 device[0]) |
135 self.deviceTypeComboBox.setItemData( |
|
136 index, device[0], self.DeviceTypeRole) |
|
137 self.deviceTypeComboBox.setItemData( |
|
138 index, device[2], self.DevicePortRole) |
|
139 |
120 else: |
140 else: |
121 self.deviceInfoLabel.setText( |
141 self.deviceInfoLabel.setText( |
122 self.tr("No supported devices detected.")) |
142 self.tr("No supported devices detected.")) |
123 |
143 |
124 self.on_deviceTypeComboBox_activated(0) |
144 self.on_deviceTypeComboBox_activated(0) |
129 Private slot handling the selection of a device type. |
149 Private slot handling the selection of a device type. |
130 |
150 |
131 @param index index of the selected device |
151 @param index index of the selected device |
132 @type int |
152 @type int |
133 """ |
153 """ |
134 deviceType = self.deviceTypeComboBox.itemData(index) |
154 deviceType = self.deviceTypeComboBox.itemData( |
|
155 index, self.DeviceTypeRole) |
135 self.deviceIconLabel.setPixmap(MicroPythonDevices.getDeviceIcon( |
156 self.deviceIconLabel.setPixmap(MicroPythonDevices.getDeviceIcon( |
136 deviceType, False)) |
157 deviceType, False)) |
137 |
158 |
138 self.__device = MicroPythonDevices.getDevice(deviceType) |
159 self.__device = MicroPythonDevices.getDevice(deviceType, self) |
139 if self.__device: |
160 self.__device.setButtons() |
140 actions = self.__device.supportedActions() |
|
141 else: |
|
142 actions = tuple() |
|
143 self.__setActionButtons(actions) |
|
144 |
161 |
145 @pyqtSlot() |
162 @pyqtSlot() |
146 def on_checkButton_clicked(self): |
163 def on_checkButton_clicked(self): |
147 """ |
164 """ |
148 Private slot to check for connected devices. |
165 Private slot to check for connected devices. |
149 """ |
166 """ |
150 self.__populateDeviceTypeComboBox() |
167 self.__populateDeviceTypeComboBox() |
151 |
168 |
152 def __setActionButtons(self, actions): |
169 def setActionButtons(self, **kwargs): |
153 """ |
170 """ |
154 Private method to set the enabled state of the various action buttons. |
171 Public method to set the enabled state of the various action buttons. |
155 |
172 |
156 @param actions tuple of supported actions out of "repl", "run", |
173 @param kwargs keyword arguments containg the enabled states (keys are |
157 "files", "chart" |
174 'run', 'repl', 'files', 'chart' |
158 @type tuple of str |
175 @type dict |
159 """ |
176 """ |
160 self.runButton.setEnabled("run" in actions) |
177 if "run" in kwargs: |
161 self.replButton.setEnabled("repl" in actions) |
178 self.runButton.setEnabled(kwargs["run"]) |
162 self.filesButton.setEnabled("files" in actions) |
179 if "repl" in kwargs: |
163 self.chartButton.setEnabled("chart" in actions) |
180 self.replButton.setEnabled(kwargs["repl"]) |
|
181 if "files" in kwargs: |
|
182 self.filesButton.setEnabled(kwargs["files"]) |
|
183 if "chart" in kwargs: |
|
184 self.chartButton.setEnabled(kwargs["chart"]) |
164 |
185 |
165 @pyqtSlot(QPoint) |
186 @pyqtSlot(QPoint) |
166 def __showContextMenu(self, pos): |
187 def __showContextMenu(self, pos): |
167 """ |
188 """ |
168 Privat slot to show the REPL context menu. |
189 Privat slot to show the REPL context menu. |
200 @pyqtSlot() |
221 @pyqtSlot() |
201 def on_replButton_clicked(self): |
222 def on_replButton_clicked(self): |
202 """ |
223 """ |
203 Private slot to connect to the selected device and start a REPL. |
224 Private slot to connect to the selected device and start a REPL. |
204 """ |
225 """ |
205 # TODO: not implemented yet |
226 if not self.__device: |
206 raise NotImplementedError |
227 E5MessageBox.critical( |
|
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 |
|
237 |
|
238 if self.__replRunning: |
|
239 self.dataReceived.disconnect(self.__processData) |
|
240 self.on_disconnectButton_clicked() |
|
241 self.__replRunning = False |
|
242 self.__device.setRepl(False) |
|
243 else: |
|
244 ok, reason = self.__device.canStartRepl() |
|
245 if not ok: |
|
246 E5MessageBox.warning( |
|
247 self, |
|
248 self.tr("Start REPL"), |
|
249 self.tr("""<p>The REPL cannot be started.</p><p>Reason:""" |
|
250 """ {0}</p>""").format(reason)) |
|
251 return |
|
252 |
|
253 if not self.__serial: |
|
254 self.replEdit.clear() |
|
255 self.dataReceived.connect(self.__processData) |
|
256 self.__openSerialLink() |
|
257 if self.__serial: |
|
258 if self.__device.forceInterrupt(): |
|
259 # send a Ctrl-B (exit raw mode) |
|
260 self.__serial.write(b'\x02') |
|
261 # send Ctrl-C (keyboard interrupt) |
|
262 self.__serial.write(b'\x03') |
207 |
263 |
208 @pyqtSlot() |
264 @pyqtSlot() |
209 def on_disconnectButton_clicked(self): |
265 def on_disconnectButton_clicked(self): |
210 """ |
266 """ |
211 Private slot to disconnect from the currently connected device. |
267 Private slot to disconnect from the currently connected device. |
212 """ |
268 """ |
213 # TODO: not implemented yet |
269 if self.__replRunning: |
214 raise NotImplementedError |
270 self.on_replButton_clicked() |
|
271 |
|
272 # TODO: add more |
|
273 |
|
274 def __disconnect(self): |
|
275 """ |
|
276 Private slot to disconnect the serial connection. |
|
277 """ |
|
278 self.__closeSerialLink() |
|
279 self.setConnected(False) |
215 |
280 |
216 def __activatePlotter(self): |
281 def __activatePlotter(self): |
217 """ |
282 """ |
218 Private method to activate a data plotter widget. |
283 Private method to activate a data plotter widget. |
219 """ |
284 """ |
369 if value < self.__currentZoom: |
434 if value < self.__currentZoom: |
370 self.replEdit.zoomOut(self.__currentZoom - value) |
435 self.replEdit.zoomOut(self.__currentZoom - value) |
371 elif value > self.__currentZoom: |
436 elif value > self.__currentZoom: |
372 self.replEdit.zoomIn(value - self.__currentZoom) |
437 self.replEdit.zoomIn(value - self.__currentZoom) |
373 self.__currentZoom = value |
438 self.__currentZoom = value |
|
439 |
|
440 def __getCurrentPort(self): |
|
441 """ |
|
442 Private method to determine the port path of the selected device. |
|
443 |
|
444 @return path of the port of the selected device |
|
445 @rtype str |
|
446 """ |
|
447 portName = self.deviceTypeComboBox.itemData( |
|
448 self.deviceTypeComboBox.currentIndex(), |
|
449 self.DevicePortRole) |
|
450 |
|
451 if Globals.isWindowsPlatform(): |
|
452 # return unchanged |
|
453 return portName |
|
454 else: |
|
455 # return with device path prepended |
|
456 return "/dev/{0}".format(portName) |
|
457 |
|
458 def __openSerialLink(self): |
|
459 """ |
|
460 Private method to open a serial link to the selected device. |
|
461 """ |
|
462 port = self.__getCurrentPort() |
|
463 self.__serial = QSerialPort() |
|
464 self.__serial.setPortName(port) |
|
465 if self.__serial.open(QIODevice.ReadWrite): |
|
466 self.__serial.setDataTerminalReady(True) |
|
467 # 115.200 baud, 8N1 |
|
468 self.__serial.setBaudRate(115200) |
|
469 self.__serial.setDataBits(QSerialPort.Data8) |
|
470 self.__serial.setParity(QSerialPort.NoParity) |
|
471 self.__serial.setStopBits(QSerialPort.OneStop) |
|
472 self.__serial.readyRead.connect(self.__readSerial) |
|
473 else: |
|
474 E5MessageBox.warning( |
|
475 self, |
|
476 self.tr("Serial Device Connect"), |
|
477 self.tr("""<p>Cannot connect to device at serial port""" |
|
478 """ <b>{0}</b>.</p>""").format(port)) |
|
479 self.__serial = None |
|
480 |
|
481 def __closeSerialLink(self): |
|
482 """ |
|
483 Private method to close the open serial connection. |
|
484 """ |
|
485 if self.__serial: |
|
486 self.__serial.close() |
|
487 self.__serial = None |
|
488 |
|
489 @pyqtSlot() |
|
490 def __readSerial(self): |
|
491 """ |
|
492 Private slot to read all available serial data and emit it with the |
|
493 "dataReceived" signal for further processing. |
|
494 """ |
|
495 data = bytes(self.__serial.readAll()) |
|
496 self.dataReceived.emit(data) |