src/eric7/MicroPython/PyBoardDevices.py

branch
eric7
changeset 9756
9854647c8c5c
parent 9755
1a09700229e7
child 9757
ab6e87f6f1c4
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 .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)

eric ide

mercurial