|
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() |