eric7/MicroPython/PyBoardDevices.py

branch
eric7
changeset 8312
800c432b34c8
parent 8220
006ee31b4835
child 8318
962bce857696
equal deleted inserted replaced
8311:4e8b98454baa 8312:800c432b34c8
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2019 - 2021 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 PyQt5.QtCore import pyqtSlot, QStandardPaths
13
14 from E5Gui import E5MessageBox, E5FileDialog
15 from E5Gui.E5Application import e5App
16 from E5Gui.E5ProcessDialog import E5ProcessDialog
17
18 from .MicroPythonDevices import MicroPythonDevice
19 from .MicroPythonWidget import HAS_QTCHART
20
21 import Utilities
22 import Preferences
23
24
25 class PyBoardDevice(MicroPythonDevice):
26 """
27 Class implementing the device for PyBoard boards.
28 """
29 DeviceVolumeName = "PYBFLASH"
30
31 FlashInstructionsURL = (
32 "https://github.com/micropython/micropython/wiki/"
33 "Pyboard-Firmware-Update"
34 )
35
36 def __init__(self, microPythonWidget, deviceType, parent=None):
37 """
38 Constructor
39
40 @param microPythonWidget reference to the main MicroPython widget
41 @type MicroPythonWidget
42 @param deviceType device type assigned to this device interface
43 @type str
44 @param parent reference to the parent object
45 @type QObject
46 """
47 super().__init__(microPythonWidget, deviceType, parent)
48
49 self.__workspace = self.__findWorkspace()
50
51 def setButtons(self):
52 """
53 Public method to enable the supported action buttons.
54 """
55 super().setButtons()
56 self.microPython.setActionButtons(
57 run=True, repl=True, files=True, chart=HAS_QTCHART)
58
59 if self.__deviceVolumeMounted():
60 self.microPython.setActionButtons(open=True, save=True)
61
62 def forceInterrupt(self):
63 """
64 Public method to determine the need for an interrupt when opening the
65 serial connection.
66
67 @return flag indicating an interrupt is needed
68 @rtype bool
69 """
70 return False
71
72 def deviceName(self):
73 """
74 Public method to get the name of the device.
75
76 @return name of the device
77 @rtype str
78 """
79 return self.tr("PyBoard")
80
81 def canStartRepl(self):
82 """
83 Public method to determine, if a REPL can be started.
84
85 @return tuple containing a flag indicating it is safe to start a REPL
86 and a reason why it cannot.
87 @rtype tuple of (bool, str)
88 """
89 return True, ""
90
91 def canStartPlotter(self):
92 """
93 Public method to determine, if a Plotter can be started.
94
95 @return tuple containing a flag indicating it is safe to start a
96 Plotter and a reason why it cannot.
97 @rtype tuple of (bool, str)
98 """
99 return True, ""
100
101 def canRunScript(self):
102 """
103 Public method to determine, if a script can be executed.
104
105 @return tuple containing a flag indicating it is safe to start a
106 Plotter and a reason why it cannot.
107 @rtype tuple of (bool, str)
108 """
109 return True, ""
110
111 def runScript(self, script):
112 """
113 Public method to run the given Python script.
114
115 @param script script to be executed
116 @type str
117 """
118 pythonScript = script.split("\n")
119 self.sendCommands(pythonScript)
120
121 def canStartFileManager(self):
122 """
123 Public method to determine, if a File Manager can be started.
124
125 @return tuple containing a flag indicating it is safe to start a
126 File Manager and a reason why it cannot.
127 @rtype tuple of (bool, str)
128 """
129 return True, ""
130
131 def supportsLocalFileAccess(self):
132 """
133 Public method to indicate file access via a local directory.
134
135 @return flag indicating file access via local directory
136 @rtype bool
137 """
138 return self.__deviceVolumeMounted()
139
140 def __deviceVolumeMounted(self):
141 """
142 Private method to check, if the device volume is mounted.
143
144 @return flag indicated a mounted device
145 @rtype bool
146 """
147 if self.__workspace and not os.path.exists(self.__workspace):
148 self.__workspace = "" # reset
149
150 return self.DeviceVolumeName in self.getWorkspace(silent=True)
151
152 def getWorkspace(self, silent=False):
153 """
154 Public method to get the workspace directory.
155
156 @param silent flag indicating silent operations
157 @type bool
158 @return workspace directory used for saving files
159 @rtype str
160 """
161 if self.__workspace:
162 # return cached entry
163 return self.__workspace
164 else:
165 self.__workspace = self.__findWorkspace(silent=silent)
166 return self.__workspace
167
168 def __findWorkspace(self, silent=False):
169 """
170 Private method to find the workspace directory.
171
172 @param silent flag indicating silent operations
173 @type bool
174 @return workspace directory used for saving files
175 @rtype str
176 """
177 # Attempts to find the path on the filesystem that represents the
178 # plugged in PyBoard board.
179 deviceDirectories = Utilities.findVolume(self.DeviceVolumeName,
180 findAll=True)
181
182 if deviceDirectories:
183 if len(deviceDirectories) == 1:
184 return deviceDirectories[0]
185 else:
186 return self.selectDeviceDirectory(deviceDirectories)
187 else:
188 # return the default workspace and give the user a warning (unless
189 # silent mode is selected)
190 if not silent:
191 E5MessageBox.warning(
192 self.microPython,
193 self.tr("Workspace Directory"),
194 self.tr("Python files for PyBoard can be edited in"
195 " place, if the device volume is locally"
196 " available. Such a volume was not found. In"
197 " place editing will not be available."
198 )
199 )
200
201 return super().getWorkspace()
202
203 def getDocumentationUrl(self):
204 """
205 Public method to get the device documentation URL.
206
207 @return documentation URL of the device
208 @rtype str
209 """
210 return Preferences.getMicroPython("MicroPythonDocuUrl")
211
212 def getFirmwareUrl(self):
213 """
214 Public method to get the device firmware download URL.
215
216 @return firmware download URL of the device
217 @rtype str
218 """
219 return Preferences.getMicroPython("MicroPythonFirmwareUrl")
220
221 def addDeviceMenuEntries(self, menu):
222 """
223 Public method to add device specific entries to the given menu.
224
225 @param menu reference to the context menu
226 @type QMenu
227 """
228 connected = self.microPython.isConnected()
229
230 act = menu.addAction(self.tr("Activate Bootloader"),
231 self.__activateBootloader)
232 act.setEnabled(connected)
233 act = menu.addAction(self.tr("List DFU-capable Devices"),
234 self.__listDfuCapableDevices)
235 act.setEnabled(not connected)
236 act = menu.addAction(self.tr("Flash MicroPython Firmware"),
237 self.__flashMicroPython)
238 act.setEnabled(not connected)
239 menu.addSeparator()
240 menu.addAction(self.tr("MicroPython Flash Instructions"),
241 self.__showFlashInstructions)
242
243 def hasFlashMenuEntry(self):
244 """
245 Public method to check, if the device has its own flash menu entry.
246
247 @return flag indicating a specific flash menu entry
248 @rtype bool
249 """
250 return True
251
252 @pyqtSlot()
253 def __showFlashInstructions(self):
254 """
255 Private slot to open the URL containing instructions for installing
256 MicroPython on the pyboard.
257 """
258 e5App().getObject("UserInterface").launchHelpViewer(
259 PyBoardDevice.FlashInstructionsURL)
260
261 def __dfuUtilAvailable(self):
262 """
263 Private method to check the availability of dfu-util.
264
265 @return flag indicating the availability of dfu-util
266 @rtype bool
267 """
268 available = False
269 program = Preferences.getMicroPython("DfuUtilPath")
270 if not program:
271 program = "dfu-util"
272 if Utilities.isinpath(program):
273 available = True
274 else:
275 if Utilities.isExecutable(program):
276 available = True
277
278 if not available:
279 E5MessageBox.critical(
280 self.microPython,
281 self.tr("dfu-util not available"),
282 self.tr("""The dfu-util firmware flashing tool"""
283 """ <b>dfu-util</b> cannot be found or is not"""
284 """ executable. Ensure it is in the search path"""
285 """ or configure it on the MicroPython"""
286 """ configuration page.""")
287 )
288
289 return available
290
291 def __showDfuEnableInstructions(self, flash=True):
292 """
293 Private method to show some instructions to enable the DFU mode.
294
295 @param flash flag indicating to show a warning message for flashing
296 @type bool
297 @return flag indicating OK to continue or abort
298 @rtype bool
299 """
300 msg = self.tr(
301 "<h3>Enable DFU Mode</h3>"
302 "<p>1. Disconnect everything from your board</p>"
303 "<p>2. Disconnect your board</p>"
304 "<p>3. Connect the DFU/BOOT0 pin with a 3.3V pin</p>"
305 "<p>4. Re-connect your board</p>"
306 "<hr />"
307 )
308
309 if flash:
310 msg += self.tr(
311 "<p><b>Warning:</b> Make sure that all other DFU capable"
312 " devices except your PyBoard are disconnected."
313 "<hr />"
314 )
315
316 msg += self.tr(
317 "<p>Press <b>OK</b> to continue...</p>"
318 )
319 res = E5MessageBox.information(
320 self.microPython,
321 self.tr("Enable DFU mode"),
322 msg,
323 E5MessageBox.StandardButtons(
324 E5MessageBox.Abort |
325 E5MessageBox.Ok))
326
327 return res == E5MessageBox.Ok
328
329 def __showDfuDisableInstructions(self):
330 """
331 Private method to show some instructions to disable the DFU mode.
332 """
333 msg = self.tr(
334 "<h3>Disable DFU Mode</h3>"
335 "<p>1. Disconnect your board</p>"
336 "<p>2. Remove the DFU jumper</p>"
337 "<p>3. Re-connect your board</p>"
338 "<hr />"
339 "<p>Press <b>OK</b> to continue...</p>"
340 )
341 E5MessageBox.information(
342 self.microPython,
343 self.tr("Disable DFU mode"),
344 msg
345 )
346
347 @pyqtSlot()
348 def __listDfuCapableDevices(self):
349 """
350 Private slot to list all DFU-capable devices.
351 """
352 if self.__dfuUtilAvailable():
353 ok2continue = self.__showDfuEnableInstructions(flash=False)
354 if ok2continue:
355 program = Preferences.getMicroPython("DfuUtilPath")
356 if not program:
357 program = "dfu-util"
358
359 args = [
360 "--list",
361 ]
362 dlg = E5ProcessDialog(
363 self.tr("'dfu-util' Output"),
364 self.tr("List DFU capable Devices")
365 )
366 res = dlg.startProcess(program, args)
367 if res:
368 dlg.exec()
369
370 @pyqtSlot()
371 def __flashMicroPython(self):
372 """
373 Private slot to flash a MicroPython firmware.
374 """
375 if self.__dfuUtilAvailable():
376 ok2continue = self.__showDfuEnableInstructions()
377 if ok2continue:
378 program = Preferences.getMicroPython("DfuUtilPath")
379 if not program:
380 program = "dfu-util"
381
382 downloadsPath = QStandardPaths.standardLocations(
383 QStandardPaths.StandardLocation.DownloadLocation)[0]
384 firmware = E5FileDialog.getOpenFileName(
385 self.microPython,
386 self.tr("Flash MicroPython Firmware"),
387 downloadsPath,
388 self.tr(
389 "MicroPython Firmware Files (*.dfu);;All Files (*)")
390 )
391 if firmware and os.path.exists(firmware):
392 args = [
393 "--alt", "0",
394 "--download", firmware,
395 ]
396 dlg = E5ProcessDialog(
397 self.tr("'dfu-util' Output"),
398 self.tr("Flash MicroPython Firmware")
399 )
400 res = dlg.startProcess(program, args)
401 if res:
402 dlg.exec()
403 self.__showDfuDisableInstructions()
404
405 @pyqtSlot()
406 def __activateBootloader(self):
407 """
408 Private slot to activate the bootloader and disconnect.
409 """
410 if self.microPython.isConnected():
411 self.microPython.commandsInterface().execute([
412 "import pyb",
413 "pyb.bootloader()",
414 ])
415 # simulate pressing the disconnect button
416 self.microPython.on_connectButton_clicked()

eric ide

mercurial