src/eric7/MicroPython/Devices/PyBoardDevices.py

branch
eric7
changeset 9756
9854647c8c5c
parent 9752
2b9546c0cbd9
child 9763
52f982c08301
equal deleted inserted replaced
9755:1a09700229e7 9756:9854647c8c5c
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)

eric ide

mercurial