1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2019 - 2023 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing the device interface class for PyBoard boards. |
|
8 """ |
|
9 |
|
10 import os |
|
11 |
|
12 from PyQt6.QtCore import QStandardPaths, QUrl, pyqtSlot |
|
13 from PyQt6.QtNetwork import QNetworkRequest |
|
14 from PyQt6.QtWidgets import QMenu |
|
15 |
|
16 from eric7 import Globals, Preferences |
|
17 from eric7.EricWidgets import EricFileDialog, EricMessageBox |
|
18 from eric7.EricWidgets.EricApplication import ericApp |
|
19 from eric7.EricWidgets.EricProcessDialog import EricProcessDialog |
|
20 from eric7.SystemUtilities import FileSystemUtilities |
|
21 |
|
22 from .MicroPythonDevices import FirmwareGithubUrls, MicroPythonDevice |
|
23 from .MicroPythonWidget import HAS_QTCHART |
|
24 |
|
25 |
|
26 class PyBoardDevice(MicroPythonDevice): |
|
27 """ |
|
28 Class implementing the device for PyBoard boards. |
|
29 """ |
|
30 |
|
31 DeviceVolumeName = "PYBFLASH" |
|
32 |
|
33 FlashInstructionsURL = ( |
|
34 "https://github.com/micropython/micropython/wiki/Pyboard-Firmware-Update" |
|
35 ) |
|
36 |
|
37 def __init__(self, microPythonWidget, deviceType, parent=None): |
|
38 """ |
|
39 Constructor |
|
40 |
|
41 @param microPythonWidget reference to the main MicroPython widget |
|
42 @type MicroPythonWidget |
|
43 @param deviceType device type assigned to this device interface |
|
44 @type str |
|
45 @param parent reference to the parent object |
|
46 @type QObject |
|
47 """ |
|
48 super().__init__(microPythonWidget, deviceType, parent) |
|
49 |
|
50 self.__workspace = self.__findWorkspace() |
|
51 |
|
52 self.__createPyboardMenu() |
|
53 |
|
54 def setButtons(self): |
|
55 """ |
|
56 Public method to enable the supported action buttons. |
|
57 """ |
|
58 super().setButtons() |
|
59 self.microPython.setActionButtons( |
|
60 run=True, repl=True, files=True, chart=HAS_QTCHART |
|
61 ) |
|
62 |
|
63 if self.__deviceVolumeMounted(): |
|
64 self.microPython.setActionButtons(open=True, save=True) |
|
65 |
|
66 def forceInterrupt(self): |
|
67 """ |
|
68 Public method to determine the need for an interrupt when opening the |
|
69 serial connection. |
|
70 |
|
71 @return flag indicating an interrupt is needed |
|
72 @rtype bool |
|
73 """ |
|
74 return False |
|
75 |
|
76 def deviceName(self): |
|
77 """ |
|
78 Public method to get the name of the device. |
|
79 |
|
80 @return name of the device |
|
81 @rtype str |
|
82 """ |
|
83 return self.tr("PyBoard") |
|
84 |
|
85 def canStartRepl(self): |
|
86 """ |
|
87 Public method to determine, if a REPL can be started. |
|
88 |
|
89 @return tuple containing a flag indicating it is safe to start a REPL |
|
90 and a reason why it cannot. |
|
91 @rtype tuple of (bool, str) |
|
92 """ |
|
93 return True, "" |
|
94 |
|
95 def canStartPlotter(self): |
|
96 """ |
|
97 Public method to determine, if a Plotter can be started. |
|
98 |
|
99 @return tuple containing a flag indicating it is safe to start a |
|
100 Plotter and a reason why it cannot. |
|
101 @rtype tuple of (bool, str) |
|
102 """ |
|
103 return True, "" |
|
104 |
|
105 def canRunScript(self): |
|
106 """ |
|
107 Public method to determine, if a script can be executed. |
|
108 |
|
109 @return tuple containing a flag indicating it is safe to start a |
|
110 Plotter and a reason why it cannot. |
|
111 @rtype tuple of (bool, str) |
|
112 """ |
|
113 return True, "" |
|
114 |
|
115 def runScript(self, script): |
|
116 """ |
|
117 Public method to run the given Python script. |
|
118 |
|
119 @param script script to be executed |
|
120 @type str |
|
121 """ |
|
122 pythonScript = script.split("\n") |
|
123 self.sendCommands(pythonScript) |
|
124 |
|
125 def canStartFileManager(self): |
|
126 """ |
|
127 Public method to determine, if a File Manager can be started. |
|
128 |
|
129 @return tuple containing a flag indicating it is safe to start a |
|
130 File Manager and a reason why it cannot. |
|
131 @rtype tuple of (bool, str) |
|
132 """ |
|
133 return True, "" |
|
134 |
|
135 def supportsLocalFileAccess(self): |
|
136 """ |
|
137 Public method to indicate file access via a local directory. |
|
138 |
|
139 @return flag indicating file access via local directory |
|
140 @rtype bool |
|
141 """ |
|
142 return self.__deviceVolumeMounted() |
|
143 |
|
144 def __deviceVolumeMounted(self): |
|
145 """ |
|
146 Private method to check, if the device volume is mounted. |
|
147 |
|
148 @return flag indicated a mounted device |
|
149 @rtype bool |
|
150 """ |
|
151 if self.__workspace and not os.path.exists(self.__workspace): |
|
152 self.__workspace = "" # reset |
|
153 |
|
154 return self.DeviceVolumeName in self.getWorkspace(silent=True) |
|
155 |
|
156 def getWorkspace(self, silent=False): |
|
157 """ |
|
158 Public method to get the workspace directory. |
|
159 |
|
160 @param silent flag indicating silent operations |
|
161 @type bool |
|
162 @return workspace directory used for saving files |
|
163 @rtype str |
|
164 """ |
|
165 if self.__workspace: |
|
166 # return cached entry |
|
167 return self.__workspace |
|
168 else: |
|
169 self.__workspace = self.__findWorkspace(silent=silent) |
|
170 return self.__workspace |
|
171 |
|
172 def __findWorkspace(self, silent=False): |
|
173 """ |
|
174 Private method to find the workspace directory. |
|
175 |
|
176 @param silent flag indicating silent operations |
|
177 @type bool |
|
178 @return workspace directory used for saving files |
|
179 @rtype str |
|
180 """ |
|
181 # Attempts to find the path on the filesystem that represents the |
|
182 # plugged in PyBoard board. |
|
183 deviceDirectories = FileSystemUtilities.findVolume( |
|
184 self.DeviceVolumeName, findAll=True |
|
185 ) |
|
186 |
|
187 if deviceDirectories: |
|
188 if len(deviceDirectories) == 1: |
|
189 return deviceDirectories[0] |
|
190 else: |
|
191 return self.selectDeviceDirectory(deviceDirectories) |
|
192 else: |
|
193 # return the default workspace and give the user a warning (unless |
|
194 # silent mode is selected) |
|
195 if not silent: |
|
196 EricMessageBox.warning( |
|
197 self.microPython, |
|
198 self.tr("Workspace Directory"), |
|
199 self.tr( |
|
200 "Python files for PyBoard can be edited in" |
|
201 " place, if the device volume is locally" |
|
202 " available. Such a volume was not found. In" |
|
203 " place editing will not be available." |
|
204 ), |
|
205 ) |
|
206 |
|
207 return super().getWorkspace() |
|
208 |
|
209 def getDocumentationUrl(self): |
|
210 """ |
|
211 Public method to get the device documentation URL. |
|
212 |
|
213 @return documentation URL of the device |
|
214 @rtype str |
|
215 """ |
|
216 return Preferences.getMicroPython("MicroPythonDocuUrl") |
|
217 |
|
218 def getFirmwareUrl(self): |
|
219 """ |
|
220 Public method to get the device firmware download URL. |
|
221 |
|
222 @return firmware download URL of the device |
|
223 @rtype str |
|
224 """ |
|
225 return Preferences.getMicroPython("MicroPythonFirmwareUrl") |
|
226 |
|
227 def __createPyboardMenu(self): |
|
228 """ |
|
229 Private method to create the pyboard submenu. |
|
230 """ |
|
231 self.__pyboardMenu = QMenu(self.tr("PyBoard Functions")) |
|
232 |
|
233 self.__showMpyAct = self.__pyboardMenu.addAction( |
|
234 self.tr("Show MicroPython Versions"), self.__showFirmwareVersions |
|
235 ) |
|
236 self.__pyboardMenu.addSeparator() |
|
237 self.__bootloaderAct = self.__pyboardMenu.addAction( |
|
238 self.tr("Activate Bootloader"), self.__activateBootloader |
|
239 ) |
|
240 self.__dfuAct = self.__pyboardMenu.addAction( |
|
241 self.tr("List DFU-capable Devices"), self.__listDfuCapableDevices |
|
242 ) |
|
243 self.__pyboardMenu.addSeparator() |
|
244 self.__flashMpyAct = self.__pyboardMenu.addAction( |
|
245 self.tr("Flash MicroPython Firmware"), self.__flashMicroPython |
|
246 ) |
|
247 self.__pyboardMenu.addAction( |
|
248 self.tr("MicroPython Flash Instructions"), self.__showFlashInstructions |
|
249 ) |
|
250 |
|
251 def addDeviceMenuEntries(self, menu): |
|
252 """ |
|
253 Public method to add device specific entries to the given menu. |
|
254 |
|
255 @param menu reference to the context menu |
|
256 @type QMenu |
|
257 """ |
|
258 connected = self.microPython.isConnected() |
|
259 linkConnected = self.microPython.isLinkConnected() |
|
260 |
|
261 self.__bootloaderAct.setEnabled(connected) |
|
262 self.__dfuAct.setEnabled(not linkConnected) |
|
263 self.__showMpyAct.setEnabled(connected) |
|
264 self.__flashMpyAct.setEnabled(not linkConnected) |
|
265 |
|
266 menu.addMenu(self.__pyboardMenu) |
|
267 |
|
268 def hasFlashMenuEntry(self): |
|
269 """ |
|
270 Public method to check, if the device has its own flash menu entry. |
|
271 |
|
272 @return flag indicating a specific flash menu entry |
|
273 @rtype bool |
|
274 """ |
|
275 return True |
|
276 |
|
277 @pyqtSlot() |
|
278 def __showFlashInstructions(self): |
|
279 """ |
|
280 Private slot to open the URL containing instructions for installing |
|
281 MicroPython on the pyboard. |
|
282 """ |
|
283 ericApp().getObject("UserInterface").launchHelpViewer( |
|
284 PyBoardDevice.FlashInstructionsURL |
|
285 ) |
|
286 |
|
287 def __dfuUtilAvailable(self): |
|
288 """ |
|
289 Private method to check the availability of dfu-util. |
|
290 |
|
291 @return flag indicating the availability of dfu-util |
|
292 @rtype bool |
|
293 """ |
|
294 available = False |
|
295 program = Preferences.getMicroPython("DfuUtilPath") |
|
296 if not program: |
|
297 program = "dfu-util" |
|
298 if FileSystemUtilities.isinpath(program): |
|
299 available = True |
|
300 else: |
|
301 if FileSystemUtilities.isExecutable(program): |
|
302 available = True |
|
303 |
|
304 if not available: |
|
305 EricMessageBox.critical( |
|
306 self.microPython, |
|
307 self.tr("dfu-util not available"), |
|
308 self.tr( |
|
309 """The dfu-util firmware flashing tool""" |
|
310 """ <b>dfu-util</b> cannot be found or is not""" |
|
311 """ executable. Ensure it is in the search path""" |
|
312 """ or configure it on the MicroPython""" |
|
313 """ configuration page.""" |
|
314 ), |
|
315 ) |
|
316 |
|
317 return available |
|
318 |
|
319 def __showDfuEnableInstructions(self, flash=True): |
|
320 """ |
|
321 Private method to show some instructions to enable the DFU mode. |
|
322 |
|
323 @param flash flag indicating to show a warning message for flashing |
|
324 @type bool |
|
325 @return flag indicating OK to continue or abort |
|
326 @rtype bool |
|
327 """ |
|
328 msg = self.tr( |
|
329 "<h3>Enable DFU Mode</h3>" |
|
330 "<p>1. Disconnect everything from your board</p>" |
|
331 "<p>2. Disconnect your board</p>" |
|
332 "<p>3. Connect the DFU/BOOT0 pin with a 3.3V pin</p>" |
|
333 "<p>4. Re-connect your board</p>" |
|
334 "<hr />" |
|
335 ) |
|
336 |
|
337 if flash: |
|
338 msg += self.tr( |
|
339 "<p><b>Warning:</b> Make sure that all other DFU capable" |
|
340 " devices except your PyBoard are disconnected." |
|
341 "<hr />" |
|
342 ) |
|
343 |
|
344 msg += self.tr("<p>Press <b>OK</b> to continue...</p>") |
|
345 res = EricMessageBox.information( |
|
346 self.microPython, |
|
347 self.tr("Enable DFU mode"), |
|
348 msg, |
|
349 EricMessageBox.Abort | EricMessageBox.Ok, |
|
350 ) |
|
351 |
|
352 return res == EricMessageBox.Ok |
|
353 |
|
354 def __showDfuDisableInstructions(self): |
|
355 """ |
|
356 Private method to show some instructions to disable the DFU mode. |
|
357 """ |
|
358 msg = self.tr( |
|
359 "<h3>Disable DFU Mode</h3>" |
|
360 "<p>1. Disconnect your board</p>" |
|
361 "<p>2. Remove the DFU jumper</p>" |
|
362 "<p>3. Re-connect your board</p>" |
|
363 "<hr />" |
|
364 "<p>Press <b>OK</b> to continue...</p>" |
|
365 ) |
|
366 EricMessageBox.information(self.microPython, self.tr("Disable DFU mode"), msg) |
|
367 |
|
368 @pyqtSlot() |
|
369 def __listDfuCapableDevices(self): |
|
370 """ |
|
371 Private slot to list all DFU-capable devices. |
|
372 """ |
|
373 if self.__dfuUtilAvailable(): |
|
374 ok2continue = self.__showDfuEnableInstructions(flash=False) |
|
375 if ok2continue: |
|
376 program = Preferences.getMicroPython("DfuUtilPath") |
|
377 if not program: |
|
378 program = "dfu-util" |
|
379 |
|
380 args = [ |
|
381 "--list", |
|
382 ] |
|
383 dlg = EricProcessDialog( |
|
384 self.tr("'dfu-util' Output"), self.tr("List DFU capable Devices") |
|
385 ) |
|
386 res = dlg.startProcess(program, args) |
|
387 if res: |
|
388 dlg.exec() |
|
389 |
|
390 @pyqtSlot() |
|
391 def __flashMicroPython(self): |
|
392 """ |
|
393 Private slot to flash a MicroPython firmware. |
|
394 """ |
|
395 if self.__dfuUtilAvailable(): |
|
396 ok2continue = self.__showDfuEnableInstructions() |
|
397 if ok2continue: |
|
398 program = Preferences.getMicroPython("DfuUtilPath") |
|
399 if not program: |
|
400 program = "dfu-util" |
|
401 |
|
402 downloadsPath = QStandardPaths.standardLocations( |
|
403 QStandardPaths.StandardLocation.DownloadLocation |
|
404 )[0] |
|
405 firmware = EricFileDialog.getOpenFileName( |
|
406 self.microPython, |
|
407 self.tr("Flash MicroPython Firmware"), |
|
408 downloadsPath, |
|
409 self.tr("MicroPython Firmware Files (*.dfu);;All Files (*)"), |
|
410 ) |
|
411 if firmware and os.path.exists(firmware): |
|
412 args = [ |
|
413 "--alt", |
|
414 "0", |
|
415 "--download", |
|
416 firmware, |
|
417 ] |
|
418 dlg = EricProcessDialog( |
|
419 self.tr("'dfu-util' Output"), |
|
420 self.tr("Flash MicroPython Firmware"), |
|
421 ) |
|
422 res = dlg.startProcess(program, args) |
|
423 if res: |
|
424 dlg.exec() |
|
425 self.__showDfuDisableInstructions() |
|
426 |
|
427 @pyqtSlot() |
|
428 def __showFirmwareVersions(self): |
|
429 """ |
|
430 Private slot to show the firmware version of the connected device and the |
|
431 available firmware version. |
|
432 """ |
|
433 if self.microPython.isConnected(): |
|
434 if self._deviceData["mpy_name"] != "micropython": |
|
435 EricMessageBox.critical( |
|
436 None, |
|
437 self.tr("Show MicroPython Versions"), |
|
438 self.tr( |
|
439 """The firmware of the connected device cannot be""" |
|
440 """ determined or the board does not run MicroPython.""" |
|
441 """ Aborting...""" |
|
442 ), |
|
443 ) |
|
444 else: |
|
445 ui = ericApp().getObject("UserInterface") |
|
446 request = QNetworkRequest(QUrl(FirmwareGithubUrls["micropython"])) |
|
447 reply = ui.networkAccessManager().head(request) |
|
448 reply.finished.connect(lambda: self.__firmwareVersionResponse(reply)) |
|
449 |
|
450 def __firmwareVersionResponse(self, reply): |
|
451 """ |
|
452 Private method handling the response of the latest version request. |
|
453 |
|
454 @param reply reference to the reply object |
|
455 @type QNetworkReply |
|
456 """ |
|
457 latestUrl = reply.url().toString() |
|
458 tag = latestUrl.rsplit("/", 1)[-1] |
|
459 while tag and not tag[0].isdecimal(): |
|
460 # get rid of leading non-decimal characters |
|
461 tag = tag[1:] |
|
462 latestVersion = Globals.versionToTuple(tag) |
|
463 |
|
464 if self._deviceData["mpy_version"] == "unknown": |
|
465 currentVersionStr = self.tr("unknown") |
|
466 currentVersion = (0, 0, 0) |
|
467 else: |
|
468 currentVersionStr = self._deviceData["mpy_version"] |
|
469 currentVersion = Globals.versionToTuple(currentVersionStr) |
|
470 |
|
471 msg = self.tr( |
|
472 "<h4>MicroPython Version Information</h4>" |
|
473 "<table>" |
|
474 "<tr><td>Installed:</td><td>{0}</td></tr>" |
|
475 "<tr><td>Available:</td><td>{1}</td></tr>" |
|
476 "</table>" |
|
477 ).format(currentVersionStr, tag) |
|
478 if currentVersion < latestVersion: |
|
479 msg += self.tr("<p><b>Update available!</b></p>") |
|
480 |
|
481 EricMessageBox.information( |
|
482 None, |
|
483 self.tr("MicroPython Version"), |
|
484 msg, |
|
485 ) |
|
486 |
|
487 @pyqtSlot() |
|
488 def __activateBootloader(self): |
|
489 """ |
|
490 Private slot to activate the bootloader and disconnect. |
|
491 """ |
|
492 if self.microPython.isConnected(): |
|
493 self.microPython.commandsInterface().execute( |
|
494 [ |
|
495 "import pyb", |
|
496 "pyb.bootloader()", |
|
497 ] |
|
498 ) |
|
499 # simulate pressing the disconnect button |
|
500 self.microPython.on_connectButton_clicked() |
|
501 |
|
502 |
|
503 def createDevice(microPythonWidget, deviceType, vid, pid, boardName, serialNumber): |
|
504 """ |
|
505 Function to instantiate a MicroPython device object. |
|
506 |
|
507 @param microPythonWidget reference to the main MicroPython widget |
|
508 @type MicroPythonWidget |
|
509 @param deviceType device type assigned to this device interface |
|
510 @type str |
|
511 @param vid vendor ID |
|
512 @type int |
|
513 @param pid product ID |
|
514 @type int |
|
515 @param boardName name of the board |
|
516 @type str |
|
517 @param serialNumber serial number of the board |
|
518 @type str |
|
519 @return reference to the instantiated device object |
|
520 @rtype PyBoardDevice |
|
521 """ |
|
522 return PyBoardDevice(microPythonWidget, deviceType) |
|