src/eric7/MicroPython/PyBoardDevices.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 8881
54e42bc2437a
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2019 - 2022 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 pyqtSlot, QStandardPaths
13
14 from EricWidgets import EricMessageBox, EricFileDialog
15 from EricWidgets.EricApplication import ericApp
16 from EricWidgets.EricProcessDialog import EricProcessDialog
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 EricMessageBox.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 ericApp().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 EricMessageBox.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 = EricMessageBox.information(
320 self.microPython,
321 self.tr("Enable DFU mode"),
322 msg,
323 EricMessageBox.Abort | EricMessageBox.Ok)
324
325 return res == EricMessageBox.Ok
326
327 def __showDfuDisableInstructions(self):
328 """
329 Private method to show some instructions to disable the DFU mode.
330 """
331 msg = self.tr(
332 "<h3>Disable DFU Mode</h3>"
333 "<p>1. Disconnect your board</p>"
334 "<p>2. Remove the DFU jumper</p>"
335 "<p>3. Re-connect your board</p>"
336 "<hr />"
337 "<p>Press <b>OK</b> to continue...</p>"
338 )
339 EricMessageBox.information(
340 self.microPython,
341 self.tr("Disable DFU mode"),
342 msg
343 )
344
345 @pyqtSlot()
346 def __listDfuCapableDevices(self):
347 """
348 Private slot to list all DFU-capable devices.
349 """
350 if self.__dfuUtilAvailable():
351 ok2continue = self.__showDfuEnableInstructions(flash=False)
352 if ok2continue:
353 program = Preferences.getMicroPython("DfuUtilPath")
354 if not program:
355 program = "dfu-util"
356
357 args = [
358 "--list",
359 ]
360 dlg = EricProcessDialog(
361 self.tr("'dfu-util' Output"),
362 self.tr("List DFU capable Devices")
363 )
364 res = dlg.startProcess(program, args)
365 if res:
366 dlg.exec()
367
368 @pyqtSlot()
369 def __flashMicroPython(self):
370 """
371 Private slot to flash a MicroPython firmware.
372 """
373 if self.__dfuUtilAvailable():
374 ok2continue = self.__showDfuEnableInstructions()
375 if ok2continue:
376 program = Preferences.getMicroPython("DfuUtilPath")
377 if not program:
378 program = "dfu-util"
379
380 downloadsPath = QStandardPaths.standardLocations(
381 QStandardPaths.StandardLocation.DownloadLocation)[0]
382 firmware = EricFileDialog.getOpenFileName(
383 self.microPython,
384 self.tr("Flash MicroPython Firmware"),
385 downloadsPath,
386 self.tr(
387 "MicroPython Firmware Files (*.dfu);;All Files (*)")
388 )
389 if firmware and os.path.exists(firmware):
390 args = [
391 "--alt", "0",
392 "--download", firmware,
393 ]
394 dlg = EricProcessDialog(
395 self.tr("'dfu-util' Output"),
396 self.tr("Flash MicroPython Firmware")
397 )
398 res = dlg.startProcess(program, args)
399 if res:
400 dlg.exec()
401 self.__showDfuDisableInstructions()
402
403 @pyqtSlot()
404 def __activateBootloader(self):
405 """
406 Private slot to activate the bootloader and disconnect.
407 """
408 if self.microPython.isConnected():
409 self.microPython.commandsInterface().execute([
410 "import pyb",
411 "pyb.bootloader()",
412 ])
413 # simulate pressing the disconnect button
414 self.microPython.on_connectButton_clicked()

eric ide

mercurial