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