|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2021 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a dialog to flash any UF2 capable device. |
|
8 """ |
|
9 |
|
10 import os |
|
11 import shutil |
|
12 |
|
13 from PyQt5.QtCore import pyqtSlot, Qt, QCoreApplication, QThread, QEventLoop |
|
14 from PyQt5.QtWidgets import QDialog |
|
15 |
|
16 from E5Gui.E5PathPicker import E5PathPickerModes |
|
17 |
|
18 from .Ui_UF2FlashDialog import Ui_UF2FlashDialog |
|
19 |
|
20 import UI.PixmapCache |
|
21 import Utilities |
|
22 |
|
23 from . import MicroPythonDevices |
|
24 |
|
25 SupportedUF2Boards = { |
|
26 "circuitpython": { |
|
27 "volumes": { |
|
28 (0x03EB, 0x2402): [ |
|
29 "SAMD21", # SAMD21 Board |
|
30 "SAME54", # SAME54 Board |
|
31 ], |
|
32 (0x04D8, 0xEC44): [ |
|
33 "PYCUBEDBOOT", # PyCubedv04 |
|
34 ], |
|
35 (0x04D8, 0xEC63): [ |
|
36 "BOOT", # CircuitBrains Basic |
|
37 ], |
|
38 (0x04D8, 0xEC64): [ |
|
39 "BOOT", # CircuitBrains Deluxe |
|
40 ], |
|
41 (0x04D8, 0xED5F): [ |
|
42 "UCHIPYBOOT", # uChip CircuitPython |
|
43 ], |
|
44 (0x04D8, 0xEDB3): [ |
|
45 "USBHUBBOOT", # Programmable USB Hub |
|
46 ], |
|
47 (0x04D8, 0xEDBE): [ |
|
48 "SAM32BOOT", # SAM32 |
|
49 ], |
|
50 (0x04D8, 0xEF66): [ |
|
51 "SENSEBOX", # senseBox MCU |
|
52 ], |
|
53 (0x1209, 0x2017): [ |
|
54 "MINISAMBOOT", # Mini SAM M4 |
|
55 ], |
|
56 (0x1209, 0x4D44): [ |
|
57 "ROBOM0BOOT", # Robo HAT MM1 |
|
58 "ROBOM4BOOT", # Robo HAT MM1 M4 |
|
59 ], |
|
60 (0x1209, 0x4DDD): [ |
|
61 "SapBOOT", # CP Sapling |
|
62 ], |
|
63 (0x1209, 0x7102): [ |
|
64 "MINISAMBOOT", # Mini SAM M0 |
|
65 ], |
|
66 (0x1209, 0x805A): [ |
|
67 "BASTBLE", # Bast BLE |
|
68 ], |
|
69 (0x1209, 0xE3E2): [ |
|
70 "StackRduino", # StackRduino M0 PRO |
|
71 ], |
|
72 (0x1209, 0xF501): [ |
|
73 "M4SHIMBOOT", # M4-Shim |
|
74 ], |
|
75 (0x16D0, 0x0CDA): [ |
|
76 "AUTOMAT", # automat |
|
77 ], |
|
78 (0x1B4F, 0x0019): [ |
|
79 "QwiicMicro", # Sparkfun Qwiic Micro |
|
80 ], |
|
81 (0x1B4F, 0x0D22): [ |
|
82 "SPARKFUN", # Sparkfun SAMD21 Mini Breakout |
|
83 ], |
|
84 (0x1B4F, 0x0D23): [ |
|
85 "SPARKFUN", # Sparkfun SAMD21 Dev Breakout |
|
86 ], |
|
87 (0x1D50, 0x6110): [ |
|
88 "ROBOTICS", # Robotics |
|
89 ], |
|
90 (0x1D50, 0x6112): [ |
|
91 "RCBOOT", # Wattuino RC |
|
92 ], |
|
93 (0x1D50, 0x6160): [ |
|
94 "BLUEMICRO", # BlueMicro |
|
95 ], |
|
96 (0x230A, 0x00E9): [ |
|
97 "TAU_BOOT", # Tau |
|
98 ], |
|
99 (0x2341, 0x0057): [ |
|
100 "NANOBOOT", # NANO 33 IoT |
|
101 ], |
|
102 (0x2341, 0x8053): [ |
|
103 "MKR1300", # MKR1300 |
|
104 ], |
|
105 (0x239A, 0x000F): [ |
|
106 "ITSYBOOT", # ItsyBitsy M0 Express |
|
107 ], |
|
108 (0x239A, 0x0013): [ |
|
109 "METROBOOT", # Metro M0 |
|
110 ], |
|
111 (0x239A, 0x0015): [ |
|
112 "FEATHERBOOT", # Feather M0 |
|
113 ], |
|
114 (0x239A, 0x0018): [ |
|
115 "CPLAYBOOT", # CPlay Express |
|
116 ], |
|
117 (0x239A, 0x001B): [ |
|
118 "FEATHERBOOT", # Feather M0 Express |
|
119 ], |
|
120 (0x239A, 0x001C): [ |
|
121 "GEMMABOOT", # Gemma M0 |
|
122 ], |
|
123 (0x239A, 0x001E): [ |
|
124 "TRINKETBOOT", # Trinket M0 |
|
125 ], |
|
126 (0x239A, 0x0021): [ |
|
127 "METROM4BOOT", # Metro M4 Express |
|
128 ], |
|
129 (0x239A, 0x0022): [ |
|
130 "ARCADE-D5", # Feather Arcade D51 |
|
131 "FEATHERBOOT", # Feather M4 Express |
|
132 ], |
|
133 (0x239A, 0x0024): [ |
|
134 "RADIOBOOT", # Radiofruit M0 |
|
135 ], |
|
136 (0x239A, 0x0027): [ |
|
137 "PIRKEYBOOT", # pIRKey M0 |
|
138 ], |
|
139 (0x239A, 0x0029): [ |
|
140 "ARGONBOOT ", # Argon |
|
141 "BORONBOOT ", # Boron |
|
142 "FTHR840BOOT", # Feather nRF52840 Express |
|
143 "MDBT50QBOOT", # Raytac MDBT50Q-RX |
|
144 "MDK840DONGL", # MDK nRF52840 USB Dongle |
|
145 "WS52840EVK", # Waveshare nRF52840 Eval |
|
146 "XENONBOOT ", # Xenon |
|
147 ], |
|
148 (0x239A, 0x002B): [ |
|
149 "ARCADE-D5", # Itsy Arcade D51 |
|
150 "ITSYM4BOOT", # ItsyBitsy M4 Express |
|
151 ], |
|
152 (0x239A, 0x002D): [ |
|
153 "CRICKITBOOT", # crickit |
|
154 ], |
|
155 (0x239A, 0x002F): [ |
|
156 "TRELM4BOOT", # Trellis M4 Express |
|
157 ], |
|
158 (0x239A, 0x0031): [ |
|
159 "GCM4BOOT", # Grand Central M4 Express |
|
160 ], |
|
161 (0x239A, 0x0033): [ |
|
162 "PYBADGEBOOT", # PyBadge |
|
163 ], |
|
164 (0x239A, 0x0034): [ |
|
165 "BADGELCBOOT", # BadgeLC |
|
166 "PEWBOOT", # PewPew |
|
167 ], |
|
168 (0x239A, 0x0035): [ |
|
169 "MKRZEROBOOT", # MKRZero |
|
170 "PORTALBOOT", # PyPortal M4 Express |
|
171 ], |
|
172 (0x239A, 0x0037): [ |
|
173 "METROM4BOOT", # Metro M4 AirLift |
|
174 ], |
|
175 (0x239A, 0x003D): [ |
|
176 "PYGAMERBOOT", # PyGamer |
|
177 ], |
|
178 (0x239A, 0x003F): [ |
|
179 "METR840BOOT", # Metro nRF52840 Express |
|
180 ], |
|
181 (0x239A, 0x0045): [ |
|
182 "CPLAYBTBOOT", # Circuit Playground nRF52840 |
|
183 ], |
|
184 (0x239A, 0x0047): [ |
|
185 "MASKM4BOOT", # Hallowing Mask M4 |
|
186 ], |
|
187 (0x239A, 0x0049): [ |
|
188 "HALLOM4BOOT", # HalloWing M4 |
|
189 ], |
|
190 (0x239A, 0x004D): [ |
|
191 "SNEKBOOT", # snekboard |
|
192 ], |
|
193 (0x239A, 0x0051): [ |
|
194 "ITSY840BOOT", # ItsyBitsy nRF52840 Express |
|
195 ], |
|
196 (0x239A, 0x0057): [ |
|
197 "SERPENTBOOT", # Serpente |
|
198 ], |
|
199 (0x239A, 0x0061): [ |
|
200 "SOLBOOT", # Sol |
|
201 ], |
|
202 (0x239A, 0x0063): [ |
|
203 "NANO33BOOT", # Nano 33 BLE |
|
204 ], |
|
205 (0x239A, 0x0065): [ |
|
206 "ND6BOOT", # ndBit6 |
|
207 ], |
|
208 (0x239A, 0x006B): [ |
|
209 "shIRtty", # shIRtty |
|
210 ], |
|
211 (0x239A, 0x0071): [ |
|
212 "CLUEBOOT", # CLUE nRF52840 |
|
213 ], |
|
214 (0x239A, 0x0079): [ |
|
215 "ARAMBOOT", # ARAMCON Badge 2019 |
|
216 ], |
|
217 (0x239A, 0x007D): [ |
|
218 "BOOKBOOT", # The Open Book Feather |
|
219 ], |
|
220 (0x239A, 0x007F): [ |
|
221 "BADGEBOOT", # OHS2020 Badge |
|
222 ], |
|
223 (0x239A, 0x0087): [ |
|
224 "FTHRSNSBOOT", # Feather nRF52840 Sense |
|
225 ], |
|
226 (0x239A, 0x0093): [ |
|
227 "ISVITABoot", # IkigaiSense Vita nRF52840 |
|
228 ], |
|
229 (0x239A, 0x0095): [ |
|
230 "UARTLOGBOOT", # UARTLogger II |
|
231 ], |
|
232 (0x239A, 0x009F): [ |
|
233 "ADM840BOOT", # AtelierDuMaker NRF52840 Breakout |
|
234 ], |
|
235 (0x239A, 0x00AF): [ |
|
236 "FLUFFBOOT", # Fluff M0 |
|
237 ], |
|
238 (0x239A, 0x00B3): [ |
|
239 "NICENANO", # nice!nano |
|
240 ], |
|
241 (0x239A, 0x00B5): [ |
|
242 "E54XBOOT", # SAME54 Xplained |
|
243 ], |
|
244 (0x239A, 0x00B9): [ |
|
245 "ND7BOOT", # ndBit7 |
|
246 ], |
|
247 (0x239A, 0x00BF): [ |
|
248 "BADGEBOOT", # BLM Badge |
|
249 ], |
|
250 (0x239A, 0x00C3): [ |
|
251 "GEMINIBOOT", # Gemini |
|
252 ], |
|
253 (0x239A, 0x00CB): [ |
|
254 "QTPY_BOOT", # QT Py M0 |
|
255 ], |
|
256 (0x239A, 0x00CD): [ |
|
257 "FTHRCANBOOT", # Feather M4 CAN Express |
|
258 ], |
|
259 (0x239A, 0x00EF): [ |
|
260 "TRINKEYBOOT", # NeoPixel Trinkey M0 |
|
261 ], |
|
262 (0x239A, 0x00F5): [ |
|
263 "STARBOOT", # Binary Star |
|
264 ], |
|
265 (0x239A, 0xB000): [ |
|
266 "HALLOWBOOT", # Hallowing M0 |
|
267 ], |
|
268 (0x239A, 0xE005): [ |
|
269 "HONKBOOT", # Big Honking Button |
|
270 ], |
|
271 (0x2886, 0x000D): [ |
|
272 "Grove Zero", # Grove Zero |
|
273 ], |
|
274 (0x2886, 0x002F): [ |
|
275 "Seeed XIAO", # Seeeduino XIAO |
|
276 "Arduino", # Seeeduino XIAO |
|
277 ], |
|
278 (0x2886, 0xF00E): [ |
|
279 "PITAYAGO", # Pitaya Go |
|
280 ], |
|
281 (0x2886, 0xF00F): [ |
|
282 "nRF52840M2", # MakerDiary nRF52840 M.2 Module |
|
283 ], |
|
284 (0x3171, 0x0100): [ |
|
285 "CMDBOOT", # COMMANDER |
|
286 ], |
|
287 }, |
|
288 "instructions": QCoreApplication.translate( |
|
289 "UF2FlashDialog", |
|
290 "<h3>CircuitPython Board</h3>" |
|
291 "<p>In order to prepare the board for flashing follow these" |
|
292 " steps:</p><ol>" |
|
293 "<li>Switch your device to 'bootloader' mode by double-pressing" |
|
294 " the reset button.</li>" |
|
295 "<li>Wait until the device has entered 'bootloader' mode.</li>" |
|
296 "<li>(If this does not happen, then try shorter or longer" |
|
297 " pauses between presses.)</li>" |
|
298 "<li>Ensure the boot volume is available (this may require" |
|
299 " mounting it).</li>" |
|
300 "<li>Select the firmware file to be flashed and click the" |
|
301 " flash button.</li>" |
|
302 "</ol>" |
|
303 ), |
|
304 "firmware": "CircuitPython", |
|
305 }, |
|
306 |
|
307 "rp2040": { |
|
308 "volumes": [ |
|
309 |
|
310 ], |
|
311 "instructions": QCoreApplication.translate( |
|
312 "UF2FlashDialog", |
|
313 "<h3>Pi Pico (RP2040) Board</h3>" |
|
314 "<p>In order to prepare the board for flashing follow these" |
|
315 " steps:</p><ol>" |
|
316 "<li>Plug in your board while holding the BOOTSEL button.</li>" |
|
317 "<li>Wait until the device has entered 'bootloader' mode.</li>" |
|
318 "<li>Ensure the boot volume is available (this may require" |
|
319 " mounting it).</li>" |
|
320 "<li>Select the firmware file to be flashed and click the" |
|
321 " flash button.</li>" |
|
322 "</ol>" |
|
323 ), |
|
324 "firmware": "MicroPython", |
|
325 }, |
|
326 } |
|
327 |
|
328 |
|
329 def getFoundDevices(boardType=""): |
|
330 """ |
|
331 Function to get the list of known serial devices supporting UF2. |
|
332 |
|
333 @param boardType specific board type to search for |
|
334 @type str |
|
335 @return list of tuples with the board type, the port description, the |
|
336 VID and PID |
|
337 @rtype list of tuple of (str, str, int, int) |
|
338 """ |
|
339 from PyQt5.QtSerialPort import QSerialPortInfo |
|
340 |
|
341 foundDevices = [] |
|
342 |
|
343 availablePorts = QSerialPortInfo.availablePorts() |
|
344 for port in availablePorts: |
|
345 vid = port.vendorIdentifier() |
|
346 pid = port.productIdentifier() |
|
347 |
|
348 if vid == 0 and pid == 0: |
|
349 # no device detected at port |
|
350 continue |
|
351 |
|
352 for board in SupportedUF2Boards: |
|
353 if not boardType or (board == boardType): |
|
354 if (vid, pid) in SupportedUF2Boards[board]["volumes"]: |
|
355 foundDevices.append(( |
|
356 board, |
|
357 port.description(), |
|
358 (vid, pid), |
|
359 )) |
|
360 |
|
361 return foundDevices |
|
362 |
|
363 |
|
364 class UF2FlashDialog(QDialog, Ui_UF2FlashDialog): |
|
365 """ |
|
366 Class implementing a dialog to flash any UF2 capable device. |
|
367 """ |
|
368 DeviceTypeRole = Qt.UserRole |
|
369 DeviceVidPidRole = Qt.UserRole + 1 |
|
370 |
|
371 def __init__(self, boardType="", parent=None): |
|
372 """ |
|
373 Constructor |
|
374 |
|
375 @param boardType specific board type to show the dialog for |
|
376 @type str |
|
377 @param parent reference to the parent widget (defaults to None) |
|
378 @type QWidget (optional) |
|
379 """ |
|
380 super(UF2FlashDialog, self).__init__(parent) |
|
381 self.setupUi(self) |
|
382 |
|
383 self.refreshButton.setIcon(UI.PixmapCache.getIcon("rescan")) |
|
384 |
|
385 self.firmwarePicker.setMode(E5PathPickerModes.OpenFileMode) |
|
386 self.firmwarePicker.setFilters( |
|
387 self.tr("MicroPython/CircuitPython Files (*.uf2);;" |
|
388 "All Files (*)")) |
|
389 |
|
390 self.bootPicker.setMode(E5PathPickerModes.DirectoryShowFilesMode) |
|
391 self.bootPicker.setEnabled(False) |
|
392 |
|
393 self.__mandatoryStyleSheet = ( |
|
394 "QLineEdit {border: 2px solid;border-color: #800000}" |
|
395 ) |
|
396 self.__manualType = "<manual>" |
|
397 |
|
398 self.__boardType = boardType |
|
399 |
|
400 self.__populate() |
|
401 |
|
402 self.__updateFlashButton() |
|
403 |
|
404 def __populate(self): |
|
405 """ |
|
406 Private method to (re-)populate the dialog. |
|
407 """ |
|
408 # save the currently selected device |
|
409 currentDevice = self.devicesComboBox.currentText() |
|
410 firmwareFile = self.firmwarePicker.text() |
|
411 |
|
412 # clear the entries first |
|
413 self.devicesComboBox.clear() |
|
414 self.firmwarePicker.clear() |
|
415 self.bootPicker.clear() |
|
416 self.infoLabel.clear() |
|
417 self.infoEdit.clear() |
|
418 |
|
419 # now populate the entries with data |
|
420 devices = getFoundDevices(boardType=self.__boardType) |
|
421 if len(devices) == 0: |
|
422 # no device detected |
|
423 devices = filter( |
|
424 lambda x: x[0] in SupportedUF2Boards, |
|
425 MicroPythonDevices.getFoundDevices()[0] |
|
426 ) |
|
427 if devices: |
|
428 self.__showSpecificInstructions(list(devices)) |
|
429 else: |
|
430 self.__showAllInstructions() |
|
431 self.devicesComboBox.addItem("") |
|
432 self.devicesComboBox.addItem(self.tr("Manual Select")) |
|
433 self.devicesComboBox.setItemData(1, self.__manualType, |
|
434 self.DeviceTypeRole) |
|
435 elif len(devices) == 1: |
|
436 self.devicesComboBox.addItem(devices[0][1]) |
|
437 self.devicesComboBox.setItemData( |
|
438 0, devices[0][0], self.DeviceTypeRole) |
|
439 self.devicesComboBox.setItemData( |
|
440 0, devices[0][2], self.DeviceVidPidRole) |
|
441 self.devicesComboBox.addItem(self.tr("Manual Select")) |
|
442 self.devicesComboBox.setItemData(1, self.__manualType, |
|
443 self.DeviceTypeRole) |
|
444 self.on_devicesComboBox_currentIndexChanged(0) |
|
445 else: |
|
446 self.devicesComboBox.addItem("") |
|
447 for index, (boardType, description, |
|
448 vidpid) in enumerate(sorted(devices), 1): |
|
449 self.devicesComboBox.addItem(description) |
|
450 self.devicesComboBox.setItemData( |
|
451 index, boardType, self.DeviceTypeRole) |
|
452 self.devicesComboBox.setItemData( |
|
453 index, vidpid, self.DeviceVidPidRole) |
|
454 self.devicesComboBox.addItem(self.tr("Manual Select")) |
|
455 self.devicesComboBox.setItemData(index + 1, self.__manualType, |
|
456 self.DeviceTypeRole) |
|
457 |
|
458 # reselect the remembered device, if it is still there |
|
459 if currentDevice: |
|
460 self.devicesComboBox.setCurrentText(currentDevice) |
|
461 self.firmwarePicker.setText(firmwareFile) |
|
462 else: |
|
463 self.devicesComboBox.setCurrentIndex(0) |
|
464 |
|
465 def __updateFlashButton(self): |
|
466 """ |
|
467 Private method to update the state of the Flash button and the retest |
|
468 button. |
|
469 """ |
|
470 firmwareFile = self.firmwarePicker.text() |
|
471 if self.devicesComboBox.currentData(self.DeviceTypeRole) is not None: |
|
472 if bool(firmwareFile) and os.path.exists(firmwareFile): |
|
473 self.firmwarePicker.setStyleSheet("") |
|
474 else: |
|
475 self.firmwarePicker.setStyleSheet(self.__mandatoryStyleSheet) |
|
476 |
|
477 if bool(self.bootPicker.text()): |
|
478 self.bootPicker.setStyleSheet("") |
|
479 else: |
|
480 self.bootPicker.setStyleSheet(self.__mandatoryStyleSheet) |
|
481 else: |
|
482 self.firmwarePicker.setStyleSheet("") |
|
483 self.bootPicker.setStyleSheet("") |
|
484 |
|
485 enable = ( |
|
486 bool(self.bootPicker.text()) and |
|
487 bool(firmwareFile) and |
|
488 os.path.exists(firmwareFile) |
|
489 ) |
|
490 self.flashButton.setEnabled(enable) |
|
491 |
|
492 def __showAllInstructions(self): |
|
493 """ |
|
494 Private method to show instructions for resetting devices to bootloader |
|
495 mode. |
|
496 """ |
|
497 self.infoLabel.setText(self.tr("Reset Instructions:")) |
|
498 |
|
499 htmlText = self.tr( |
|
500 "<h4>No known devices detected.</h4>" |
|
501 "<p>Follow the appropriate instructions below to set <b>one</b>" |
|
502 " board into 'bootloader' mode. Press <b>Refresh</b> when ready." |
|
503 "</p>" |
|
504 ) |
|
505 for boardType in SupportedUF2Boards: |
|
506 htmlText += "<hr/>" + SupportedUF2Boards[boardType]["instructions"] |
|
507 self.infoEdit.setHtml(htmlText) |
|
508 |
|
509 def __showSpecificInstructions(self, devices): |
|
510 """ |
|
511 Private method to show instructions for resetting devices to bootloader |
|
512 mode for a list of detected devices. |
|
513 |
|
514 @param devices list of detected devices |
|
515 @type list of str |
|
516 """ |
|
517 boardTypes = set(x[0] for x in devices) |
|
518 |
|
519 self.infoLabel.setText(self.tr("Reset Instructions:")) |
|
520 |
|
521 if self.__boardType: |
|
522 htmlText = self.tr( |
|
523 "<h4>Flash {0} Firmware</h4>" |
|
524 "<p>Follow the instructions below to set <b>one</b> board into" |
|
525 " 'bootloader' mode. Press <b>Refresh</b> when ready.</p>" |
|
526 "<hr/>{1}" |
|
527 ).format( |
|
528 SupportedUF2Boards[self.__boardType]["firmware"], |
|
529 SupportedUF2Boards[self.__boardType]["instructions"], |
|
530 ) |
|
531 else: |
|
532 htmlText = self.tr( |
|
533 "<h4>Potentially UF2 capable devices found</h4>" |
|
534 "<p>Found these potentially UF2 capable devices:</p>" |
|
535 "<ul><li>{0}</li></ul>" |
|
536 "<p>Follow the instructions below to set <b>one</b> board into" |
|
537 " 'bootloader' mode. Press <b>Refresh</b> when ready.</p>" |
|
538 ).format( |
|
539 "</li><li>".join(sorted(x[1] for x in devices)) |
|
540 ) |
|
541 for boardType in sorted(boardTypes): |
|
542 htmlText += ( |
|
543 "<hr/>" + SupportedUF2Boards[boardType]["instructions"] |
|
544 ) |
|
545 self.infoEdit.setHtml(htmlText) |
|
546 |
|
547 def __showTypedInstructions(self, boardType): |
|
548 """ |
|
549 Private method to show instructions for resetting devices to bootloader |
|
550 mode for a specific board type. |
|
551 |
|
552 @param boardType type of the board to show instructions for |
|
553 @type str |
|
554 """ |
|
555 self.infoLabel.setText(self.tr("Reset Instructions:")) |
|
556 |
|
557 htmlText = self.tr( |
|
558 "<h4>No known devices detected.</h4>" |
|
559 "<p>Follow the instructions below to set <b>one</b> board into" |
|
560 " 'bootloader' mode. Press <b>Refresh</b> when ready.</p>" |
|
561 ) |
|
562 htmlText += "<hr/>" + SupportedUF2Boards[boardType]["instructions"] |
|
563 self.infoEdit.setHtml(htmlText) |
|
564 |
|
565 def __showManualInstructions(self): |
|
566 """ |
|
567 Private method to show instructions for flashing devices manually. |
|
568 """ |
|
569 self.infoLabel.setText(self.tr("Flash Instructions:")) |
|
570 |
|
571 htmlText = self.tr( |
|
572 "<h4>Flash method 'manual' selected.</h4>" |
|
573 "<p>Follow the instructions below to flash a device by entering" |
|
574 "the data manually.</p><ol>" |
|
575 "<li>Change the device to 'bootloader' mode.</li>" |
|
576 "<li>Wait until the device has entered 'bootloader' mode.</li>" |
|
577 "<li>Ensure the boot volume is available (this may require" |
|
578 " mounting it) and select its path.</li>" |
|
579 "<li>Select the firmware file to be flashed and click the" |
|
580 " flash button.</li>" |
|
581 "</ol>" |
|
582 ) |
|
583 for boardType in SupportedUF2Boards: |
|
584 htmlText += "<hr/>" + SupportedUF2Boards[boardType]["instructions"] |
|
585 self.infoEdit.setHtml(htmlText) |
|
586 |
|
587 def __showNoVolumeInformation(self, volumes): |
|
588 """ |
|
589 Private method to show information about the expected boot volume(s). |
|
590 |
|
591 @param volumes list of expected volume names |
|
592 @type list of str |
|
593 """ |
|
594 self.infoLabel.setText(self.tr("Boot Volume not found:")) |
|
595 |
|
596 htmlText = self.tr( |
|
597 "<h4>No Boot Volume detected.</h4>" |
|
598 "<p>Please ensure that the boot volume of the device to be flashed" |
|
599 " is available. " |
|
600 ) |
|
601 if len(volumes) == 1: |
|
602 htmlText += self.tr( |
|
603 "This volume should be named <b>{0}</b>." |
|
604 " Press <b>Refresh</b> when ready.</p>" |
|
605 ).format(volumes[0]) |
|
606 else: |
|
607 htmlText += self.tr( |
|
608 "This volume should have one of these names.</p>" |
|
609 "<ul><li>{0}</li></ul>" |
|
610 "<p>Press <b>Refresh</b> when ready.</p>" |
|
611 ).format("</li><li>".join(sorted(volumes))) |
|
612 self.infoEdit.setHtml(htmlText) |
|
613 |
|
614 def __showMultipleVolumesInformation(self, volumePaths): |
|
615 """ |
|
616 Private method to show information because multiple devices of the |
|
617 same type are ready for flashing. |
|
618 |
|
619 Note: This is a dangerous situation! |
|
620 |
|
621 @param volumePaths list of volume paths |
|
622 @type list of str |
|
623 """ |
|
624 self.infoLabel.setText(self.tr("Multiple Boot Volumes found:")) |
|
625 |
|
626 htmlText = self.tr( |
|
627 "<h4>Multiple Boot Volumes were found</h4>" |
|
628 "<p>These volume paths were found.</p><ul><li>{0}</li></ul>" |
|
629 "<p>Please ensure that only one device of a type is ready for" |
|
630 " flashing. Press <b>Refresh</b> when ready.</p>" |
|
631 ).format("</li><li>".join(sorted(volumePaths))) |
|
632 self.infoEdit.setHtml(htmlText) |
|
633 |
|
634 @pyqtSlot() |
|
635 def on_flashButton_clicked(self): |
|
636 """ |
|
637 Private slot to flash the selected MicroPython or CircuitPython |
|
638 firmware onto the device. |
|
639 """ |
|
640 boardType = self.devicesComboBox.currentData(self.DeviceTypeRole) |
|
641 firmwarePath = self.firmwarePicker.text() |
|
642 volumePath = self.bootPicker.text() |
|
643 if os.path.exists(firmwarePath) and os.path.exists(volumePath): |
|
644 firmwareType = SupportedUF2Boards[boardType]["firmware"] |
|
645 self.infoLabel.setText( |
|
646 self.tr("Flashing {0}").format(firmwareType)) |
|
647 self.infoEdit.setHtml(self.tr( |
|
648 "<p>Flashing the {0} firmware to the device. Please wait" |
|
649 " until the device resets automatically</p>." |
|
650 ).format(firmwareType)) |
|
651 QCoreApplication.processEvents(QEventLoop.ExcludeUserInputEvents) |
|
652 shutil.copy2(firmwarePath, volumePath) |
|
653 QThread.sleep(1) |
|
654 self.on_refreshButton_clicked() |
|
655 |
|
656 @pyqtSlot() |
|
657 def on_refreshButton_clicked(self): |
|
658 """ |
|
659 Private slot to refresh the dialog. |
|
660 """ |
|
661 self.__populate() |
|
662 |
|
663 @pyqtSlot(int) |
|
664 def on_devicesComboBox_currentIndexChanged(self, index): |
|
665 """ |
|
666 Private slot to handle the selection of a board. |
|
667 |
|
668 @param index selected index |
|
669 @type int |
|
670 """ |
|
671 vidpid = self.devicesComboBox.itemData(index, self.DeviceVidPidRole) |
|
672 boardType = self.devicesComboBox.itemData(index, self.DeviceTypeRole) |
|
673 |
|
674 self.bootPicker.setEnabled(boardType == self.__manualType) |
|
675 if boardType == self.__manualType: |
|
676 self.__showManualInstructions() |
|
677 |
|
678 if vidpid is None: |
|
679 if boardType is None: |
|
680 self.bootPicker.clear() |
|
681 else: |
|
682 volumes = SupportedUF2Boards[boardType]["volumes"][vidpid] |
|
683 foundVolumes = [] |
|
684 for volume in volumes: |
|
685 foundVolumes += Utilities.findVolume(volume, findAll=True) |
|
686 |
|
687 if len(foundVolumes) == 0: |
|
688 self.__showNoVolumeInformation(volumes) |
|
689 self.bootPicker.clear() |
|
690 elif len(foundVolumes) == 1: |
|
691 self.bootPicker.setText(foundVolumes[0]) |
|
692 else: |
|
693 self.__showMultipleVolumesInformation() |
|
694 self.bootPicker.clear() |
|
695 |
|
696 self.__updateFlashButton() |
|
697 |
|
698 @pyqtSlot(str) |
|
699 def on_firmwarePicker_textChanged(self, text): |
|
700 """ |
|
701 Private slot handling a change of the firmware file. |
|
702 |
|
703 @param text current text of the firmware edit |
|
704 @type str |
|
705 """ |
|
706 self.__updateFlashButton() |
|
707 |
|
708 @pyqtSlot(str) |
|
709 def on_bootPicker_textChanged(self, text): |
|
710 """ |
|
711 Private slot handling a change of the boot volume. |
|
712 |
|
713 @param text current text of the boot volume edit |
|
714 @type str |
|
715 """ |
|
716 self.__updateFlashButton() |