|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2021 - 2022 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 import contextlib |
|
13 |
|
14 from PyQt6.QtCore import pyqtSlot, Qt, QCoreApplication, QThread, QEventLoop |
|
15 from PyQt6.QtWidgets import QDialog |
|
16 |
|
17 from EricWidgets.EricPathPicker import EricPathPickerModes |
|
18 from EricWidgets.EricApplication import ericApp |
|
19 |
|
20 from .Ui_UF2FlashDialog import Ui_UF2FlashDialog |
|
21 |
|
22 import UI.PixmapCache |
|
23 import Utilities |
|
24 |
|
25 from . import MicroPythonDevices |
|
26 |
|
27 SupportedUF2Boards = { |
|
28 "circuitpython": { |
|
29 "volumes": { |
|
30 (0x03EB, 0x2402): [ |
|
31 "SAMD21", # SAMD21 Board |
|
32 "SAME54", # SAME54 Board |
|
33 ], |
|
34 (0x04D8, 0xEC44): [ |
|
35 "PYCUBEDBOOT", # PyCubedv04 |
|
36 ], |
|
37 (0x04D8, 0xEC63): [ |
|
38 "BOOT", # CircuitBrains Basic |
|
39 ], |
|
40 (0x04D8, 0xEC64): [ |
|
41 "BOOT", # CircuitBrains Deluxe |
|
42 ], |
|
43 (0x04D8, 0xED5F): [ |
|
44 "UCHIPYBOOT", # uChip CircuitPython |
|
45 ], |
|
46 (0x04D8, 0xEDB3): [ |
|
47 "USBHUBBOOT", # Programmable USB Hub |
|
48 ], |
|
49 (0x04D8, 0xEDBE): [ |
|
50 "SAM32BOOT", # SAM32 |
|
51 ], |
|
52 (0x04D8, 0xEF66): [ |
|
53 "SENSEBOX", # senseBox MCU |
|
54 ], |
|
55 (0x1209, 0x2017): [ |
|
56 "MINISAMBOOT", # Mini SAM M4 |
|
57 ], |
|
58 (0x1209, 0x3252): [ |
|
59 "MCBS2OMBOOT", # Module Clip w/Wroom |
|
60 ], |
|
61 (0x1209, 0x3253): [ |
|
62 "MCBS2ERBOOT", # Module Clip w/Wrover |
|
63 ], |
|
64 (0x1209, 0x4D44): [ |
|
65 "ROBOM0BOOT", # Robo HAT MM1 |
|
66 "ROBOM4BOOT", # Robo HAT MM1 M4 |
|
67 ], |
|
68 (0x1209, 0x4DDD): [ |
|
69 "SapBOOT", # CP Sapling |
|
70 ], |
|
71 (0x1209, 0x7102): [ |
|
72 "MINISAMBOOT", # Mini SAM M0 |
|
73 ], |
|
74 (0x1209, 0x7A01): [ |
|
75 "MIKOTO-BOOT", # Mikoto nRF52840 |
|
76 ], |
|
77 (0x1209, 0x805A): [ |
|
78 "BASTBLE", # Bast BLE |
|
79 ], |
|
80 (0x1209, 0xE3E2): [ |
|
81 "StackRduino", # StackRduino M0 PRO |
|
82 ], |
|
83 (0x1209, 0xF501): [ |
|
84 "M4SHIMBOOT", # M4-Shim |
|
85 ], |
|
86 (0x15BA, 0x28DC): [ |
|
87 "OLMLIPOBOOT", # ESP32S2 DevKit Lipo |
|
88 ], |
|
89 (0x16D0, 0x0CDA): [ |
|
90 "AUTOMAT", # automat |
|
91 ], |
|
92 (0x1B4F, 0x0022): [ |
|
93 "SFMM852BOOT", # MicroMod nRF52840 |
|
94 ], |
|
95 (0x1B4F, 0x002C): [ |
|
96 "THNG+32BOOT", # Thing Plus - STM32 |
|
97 ], |
|
98 (0x1B4F, 0x0D22): [ |
|
99 "SPARKFUN", # SAMD21 Mini Breakout |
|
100 ], |
|
101 (0x1B4F, 0x0D23): [ |
|
102 "SPARKFUN", # SAMD21 Dev Breakout |
|
103 ], |
|
104 (0x1D50, 0x6110): [ |
|
105 "ROBOTICS", # Robotics |
|
106 ], |
|
107 (0x1D50, 0x6112): [ |
|
108 "RCBOOT", # Wattuino RC |
|
109 ], |
|
110 (0x1D50, 0x6157): [ |
|
111 "BBOARDBOOT", # nRF52840 BBoard |
|
112 ], |
|
113 (0x1D50, 0x6160): [ |
|
114 "BLUEMICRO", # BlueMicro |
|
115 ], |
|
116 (0x1D50, 0x616F): [ |
|
117 "BLUEMICRO", # BlueMicro |
|
118 ], |
|
119 (0x1FC9, 0x0094): [ |
|
120 "DblM33BOOT", # Double M33 |
|
121 "LPC5528BOOT", # LPCXpresso 55s28 |
|
122 "LPC5569BOOT", # LPCXpresso 55s69 |
|
123 ], |
|
124 (0x1FC9, 0x0154): [ |
|
125 "K32L2BOOT", # FRDM-K32L2B3 |
|
126 "K32L2BOOT", # KUIIC |
|
127 ], |
|
128 (0x230A, 0x00E9): [ |
|
129 "TAU_BOOT", # Tau |
|
130 ], |
|
131 (0x2341, 0x0057): [ |
|
132 "NANOBOOT", # NANO 33 IoT |
|
133 ], |
|
134 (0x2341, 0x8053): [ |
|
135 "MKR1300", # MKR1300 |
|
136 ], |
|
137 (0x239A, 0x000F): [ |
|
138 "ITSYBOOT", # ItsyBitsy M0 Express |
|
139 ], |
|
140 (0x239A, 0x0013): [ |
|
141 "METROBOOT", # Metro M0 |
|
142 ], |
|
143 (0x239A, 0x0015): [ |
|
144 "FEATHERBOOT", # Feather M0 |
|
145 ], |
|
146 (0x239A, 0x0018): [ |
|
147 "CPLAYBOOT", # CPlay Express |
|
148 ], |
|
149 (0x239A, 0x001B): [ |
|
150 "FEATHERBOOT", # Feather M0 Express |
|
151 ], |
|
152 (0x239A, 0x001C): [ |
|
153 "GEMMABOOT", # Gemma M0 |
|
154 ], |
|
155 (0x239A, 0x001E): [ |
|
156 "TRINKETBOOT", # Trinket M0 |
|
157 ], |
|
158 (0x239A, 0x0021): [ |
|
159 "METROM4BOOT", # Metro M4 Express |
|
160 ], |
|
161 (0x239A, 0x0022): [ |
|
162 "ARCADE-D5", # Feather Arcade D51 |
|
163 "FEATHERBOOT", # Feather M4 Express |
|
164 ], |
|
165 (0x239A, 0x0024): [ |
|
166 "RADIOBOOT", # Radiofruit M0 |
|
167 ], |
|
168 (0x239A, 0x0027): [ |
|
169 "PIRKEYBOOT", # pIRKey M0 |
|
170 ], |
|
171 (0x239A, 0x0029): [ |
|
172 "ARGONBOOT ", # Argon |
|
173 "BORONBOOT ", # Boron |
|
174 "FTHR840BOOT", # Feather nRF52840 Express |
|
175 "MDK840DONGL", # MDK nRF52840 USB Dongle |
|
176 "WS52840EVK", # Waveshare nRF52840 Eval |
|
177 "XENONBOOT ", # Xenon |
|
178 ], |
|
179 (0x239A, 0x002B): [ |
|
180 "ARCADE-D5", # Itsy Arcade D51 |
|
181 "ITSYM4BOOT", # ItsyBitsy M4 Express |
|
182 ], |
|
183 (0x239A, 0x002D): [ |
|
184 "CRICKITBOOT", # crickit |
|
185 ], |
|
186 (0x239A, 0x002F): [ |
|
187 "TRELM4BOOT", # Trellis M4 Express |
|
188 ], |
|
189 (0x239A, 0x0031): [ |
|
190 "GCM4BOOT", # Grand Central M4 Express |
|
191 ], |
|
192 (0x239A, 0x0033): [ |
|
193 "PYBADGEBOOT", # PyBadge |
|
194 ], |
|
195 (0x239A, 0x0034): [ |
|
196 "BADGELCBOOT", # BadgeLC |
|
197 "PEWBOOT", # PewPew |
|
198 ], |
|
199 (0x239A, 0x0035): [ |
|
200 "MKRZEROBOOT", # MKRZero |
|
201 "PORTALBOOT", # PyPortal M4 Express |
|
202 ], |
|
203 (0x239A, 0x0037): [ |
|
204 "METROM4BOOT", # Metro M4 AirLift |
|
205 ], |
|
206 (0x239A, 0x003D): [ |
|
207 "PYGAMERBOOT", # PyGamer |
|
208 ], |
|
209 (0x239A, 0x003F): [ |
|
210 "METR840BOOT", # Metro nRF52840 Express |
|
211 ], |
|
212 (0x239A, 0x0045): [ |
|
213 "CPLAYBTBOOT", # Circuit Playground nRF52840 |
|
214 ], |
|
215 (0x239A, 0x0047): [ |
|
216 "MASKM4BOOT", # Hallowing Mask M4 |
|
217 ], |
|
218 (0x239A, 0x0049): [ |
|
219 "HALLOM4BOOT", # HalloWing M4 |
|
220 ], |
|
221 (0x239A, 0x004D): [ |
|
222 "SNEKBOOT", # snekboard |
|
223 ], |
|
224 (0x239A, 0x0051): [ |
|
225 "ITSY840BOOT", # ItsyBitsy nRF52840 Express |
|
226 ], |
|
227 (0x239A, 0x0057): [ |
|
228 "SERPENTBOOT", # Serpente |
|
229 ], |
|
230 (0x239A, 0x0059): [ |
|
231 "FTHR405BOOT", # Feather STM32F405 Express |
|
232 ], |
|
233 (0x239A, 0x005D): [ |
|
234 "BlackPill", # STM32F401CxUx |
|
235 "STMF411BOOT", # STM32F411 Discovery |
|
236 ], |
|
237 (0x239A, 0x0061): [ |
|
238 "SOLBOOT", # Sol |
|
239 ], |
|
240 (0x239A, 0x0063): [ |
|
241 "NANO33BOOT", # Nano 33 BLE |
|
242 ], |
|
243 (0x239A, 0x0065): [ |
|
244 "ND6BOOT", # ndBit6 |
|
245 ], |
|
246 (0x239A, 0x0069): [ |
|
247 "STMF411BOOT", # STM32F411 BlackPill |
|
248 ], |
|
249 (0x239A, 0x006B): [ |
|
250 "shIRtty", # shIRtty |
|
251 ], |
|
252 (0x239A, 0x0071): [ |
|
253 "CLUEBOOT", # CLUE nRF52840 |
|
254 ], |
|
255 (0x239A, 0x0077): [ |
|
256 "RT1010BOOT", # RT1010 EVK |
|
257 ], |
|
258 (0x239A, 0x0079): [ |
|
259 "ARAMBOOT", # ARAMCON Badge 2019 |
|
260 ], |
|
261 (0x239A, 0x007B): [ |
|
262 "ARAMBOOT", # ARAMCON2 Badge |
|
263 ], |
|
264 (0x239A, 0x007D): [ |
|
265 "BOOKBOOT", # The Open Book Feather |
|
266 ], |
|
267 (0x239A, 0x007F): [ |
|
268 "BADGEBOOT", # OHS2020 Badge |
|
269 ], |
|
270 (0x239A, 0x0081): [ |
|
271 "RT1020BOOT", # RT1020 EVK |
|
272 "RT1024BOOT", # RT1024 EVK |
|
273 ], |
|
274 (0x239A, 0x0083): [ |
|
275 "RT1060BOOT", # RT1060 EVK |
|
276 "RT1064BOOT", # RT1064 EVK |
|
277 ], |
|
278 (0x239A, 0x0087): [ |
|
279 "FTHRSNSBOOT", # Feather nRF52840 Sense |
|
280 ], |
|
281 (0x239A, 0x0093): [ |
|
282 "ISVITABoot", # IkigaiSense Vita nRF52840 |
|
283 ], |
|
284 (0x239A, 0x0095): [ |
|
285 "UARTLOGBOOT", # UARTLogger II |
|
286 ], |
|
287 (0x239A, 0x009F): [ |
|
288 "ADM840BOOT", # AtelierDuMaker NRF52840 Breakout |
|
289 ], |
|
290 (0x239A, 0x00A5): [ |
|
291 "S3DKC1BOOT", # ESP32S3 DevKitC 1 |
|
292 "S3DKM1BOOT", # ESP32S3 DevKitM 1 |
|
293 "SAOLA1RBOOT", # Saola 1R WROVER |
|
294 ], |
|
295 (0x239A, 0x00A7): [ |
|
296 "SAOLA1MBOOT", # Saola 1M WROOM |
|
297 ], |
|
298 (0x239A, 0x00AB): [ |
|
299 "UFTHRS2BOOT", # FeatherS2 |
|
300 ], |
|
301 (0x239A, 0x00AF): [ |
|
302 "FLUFFBOOT", # Fluff M0 |
|
303 ], |
|
304 (0x239A, 0x00B3): [ |
|
305 "NICENANO", # nice!nano |
|
306 ], |
|
307 (0x239A, 0x00B5): [ |
|
308 "E54XBOOT", # SAME54 Xplained |
|
309 ], |
|
310 (0x239A, 0x00B9): [ |
|
311 "ND7BOOT", # ndBit7 |
|
312 ], |
|
313 (0x239A, 0x00BB): [ |
|
314 "MDBT50QBOOT", # Raytac MDBT50Q Demo Board 40 |
|
315 ], |
|
316 (0x239A, 0x00BF): [ |
|
317 "BADGEBOOT", # BLM Badge |
|
318 ], |
|
319 (0x239A, 0x00C3): [ |
|
320 "GEMINIBOOT", # Gemini |
|
321 ], |
|
322 (0x239A, 0x00C5): [ |
|
323 "MICROS2BOOT", # microS2 |
|
324 ], |
|
325 (0x239A, 0x00C7): [ |
|
326 "KALUGA1BOOT", # Kaluga 1 |
|
327 ], |
|
328 (0x239A, 0x00C9): [ |
|
329 "MATRIXBOOT", # Matrix Portal M4 |
|
330 ], |
|
331 (0x239A, 0x00CB): [ |
|
332 "QTPY_BOOT", # QT Py M0 |
|
333 ], |
|
334 (0x239A, 0x00CD): [ |
|
335 "FTHRCANBOOT", # Feather M4 CAN Express |
|
336 ], |
|
337 (0x239A, 0x00DE): [ |
|
338 "NANOESPBOOT", # nanoESP32-S2 WROOM |
|
339 ], |
|
340 (0x239A, 0x00DF): [ |
|
341 "METROS2BOOT", # Metro ESP32-S2 |
|
342 ], |
|
343 (0x239A, 0x00E1): [ |
|
344 "METROM7BOOT", # Metro M7 iMX RT1011 |
|
345 ], |
|
346 (0x239A, 0x00E5): [ |
|
347 "MAGTAGBOOT", # Metro MagTag 2.9 Grayscale |
|
348 "MAGTAGBOOT", # MagTag 2.9 Grayscale |
|
349 ], |
|
350 (0x239A, 0x00EB): [ |
|
351 "FTHRS2BOOT", # Feather ESP32-S2 |
|
352 ], |
|
353 (0x239A, 0x00ED): [ |
|
354 "FTHRS2BOOT", # Feather ESP32-S2 Reverse TFT |
|
355 ], |
|
356 (0x239A, 0x00EF): [ |
|
357 "TRINKEYBOOT", # NeoPixel Trinkey M0 |
|
358 ], |
|
359 (0x239A, 0x00F5): [ |
|
360 "STARBOOT", # Binary Star |
|
361 ], |
|
362 (0x239A, 0x00F9): [ |
|
363 "HOUSEBOOT", # FunHouse |
|
364 ], |
|
365 (0x239A, 0x00FB): [ |
|
366 "TRINKEYBOOT", # Rotary Trinkey M0 |
|
367 ], |
|
368 (0x239A, 0x00FF): [ |
|
369 "TRINKEYBOOT", # NeoKey Trinkey M0 |
|
370 ], |
|
371 (0x239A, 0x0101): [ |
|
372 "TRINKEYBOOT", # Slide Trinkey M0 |
|
373 ], |
|
374 (0x239A, 0x0103): [ |
|
375 "TRINKEYBOOT", # ProxSense Trinkey M0 |
|
376 ], |
|
377 (0x239A, 0x010B): [ |
|
378 "MDBT50QBOOT", # Raytac MDBT50Q-RX |
|
379 ], |
|
380 (0x239A, 0x010D): [ |
|
381 "GLASSESBOOT", # LED Glasses Driver nRF52840 |
|
382 ], |
|
383 (0x239A, 0x010F): [ |
|
384 "FTHRS2BOOT", # Feather ESP32-S2 TFT |
|
385 ], |
|
386 (0x239A, 0x0111): [ |
|
387 "QTPYS2BOOT", # QT Py ESP32-S2 |
|
388 ], |
|
389 (0x239A, 0x0113): [ |
|
390 "FTHRS3BOOT", # Feather ESP32-S3 No PSRAM |
|
391 ], |
|
392 (0x239A, 0x0115): [ |
|
393 "FEATHERBOOT", # Feather M4 Adalogger |
|
394 ], |
|
395 (0x239A, 0x0117): [ |
|
396 "CAMERABOOT", # Camera |
|
397 ], |
|
398 (0x239A, 0x0119): [ |
|
399 "QTPYS3BOOT", # QT Py ESP32-S3 |
|
400 ], |
|
401 (0x239A, 0x800B): [ |
|
402 "ATMZBOOT", # ATMegaZero ESP32-S2 |
|
403 ], |
|
404 (0x239A, 0xB000): [ |
|
405 "HALLOWBOOT", # Hallowing M0 |
|
406 ], |
|
407 (0x239A, 0xE005): [ |
|
408 "HONKBOOT", # Big Honking Button |
|
409 ], |
|
410 (0x2886, 0x000D): [ |
|
411 "Grove Zero", # Grove Zero |
|
412 ], |
|
413 (0x2886, 0x002F): [ |
|
414 "Seeed XIAO", # Seeeduino XIAO |
|
415 ], |
|
416 (0x2886, 0xF00E): [ |
|
417 "PITAYAGO", # Pitaya Go |
|
418 ], |
|
419 (0x2886, 0xF00F): [ |
|
420 "M60KEYBOARD", # MakerDiary M60 Mechanical Keyboard |
|
421 "nRF52840M2", # MakerDiary nRF52840 M.2 Module |
|
422 ], |
|
423 (0x303A, 0x7000): [ |
|
424 "ESPHMI1BOOT", # HMI 1 |
|
425 ], |
|
426 (0x303A, 0x8005): [ |
|
427 "TINYS2BOOT", # TinyS2 |
|
428 ], |
|
429 (0x303A, 0x8008): [ |
|
430 "TTGOS2BOOT", # TTGO_T8_S2_Display |
|
431 ], |
|
432 (0x303A, 0x800E): [ |
|
433 "CCMBRISBOOT", # CucumberRIS v1.1 |
|
434 ], |
|
435 (0x303A, 0x80B0): [ |
|
436 "RD00RBOOT", # Reference Design RD00 |
|
437 ], |
|
438 (0x303A, 0x80B3): [ |
|
439 "NANOESPBOOT", # nanoESP32-S2 WROVER |
|
440 ], |
|
441 (0x303A, 0x80B5): [ |
|
442 "FS2NEOBOOT", # FeatherS2 Neo |
|
443 ], |
|
444 (0x303A, 0x80B6): [ |
|
445 "MORPHBOOT", # MORPHESP-240 |
|
446 ], |
|
447 (0x303A, 0x80C4): [ |
|
448 "S2MINIBOOT", # S2 Mini |
|
449 ], |
|
450 (0x303A, 0x80C7): [ |
|
451 "S2PICOBOOT", # S2 Pico |
|
452 ], |
|
453 (0x303A, 0x80D2): [ |
|
454 "TINYS3BOOT", # TinyS3 |
|
455 ], |
|
456 (0x303A, 0x80D5): [ |
|
457 "PROS3BOOT", # ProS3 |
|
458 ], |
|
459 (0x303A, 0x80D8): [ |
|
460 "UFTHRS3BOOT", # FeatherS3 |
|
461 ], |
|
462 (0x303A, 0x80DA): [ |
|
463 "HEXKYBOOT", # HexKy-S2 |
|
464 ], |
|
465 (0x303A, 0x80DE): [ |
|
466 "LEAFS3BOOT", # BPI-Leaf-S3 |
|
467 ], |
|
468 (0x303A, 0x80E1): [ |
|
469 "LEAFS2BOOT", # BPI-Leaf-S2 |
|
470 ], |
|
471 (0x303A, 0x80E4): [ |
|
472 "BITS2BOOT", # BPI-BIT-S2 |
|
473 ], |
|
474 (0x303A, 0x80EB): [ |
|
475 "TTGOS2BOOT", # TTGO_T8_S2_WROOM |
|
476 ], |
|
477 (0x303A, 0x80EE): [ |
|
478 "TTGOS2BOOT", # TTGO_T8_S2 |
|
479 ], |
|
480 (0x3171, 0x0100): [ |
|
481 "CMDBOOT", # COMMANDER |
|
482 ], |
|
483 (0x80E7, 0x8111): [ |
|
484 "IOTS2BOOT", # HiiBot IoTs2 |
|
485 ], |
|
486 (0xCAFE, 0xFFFF): [ |
|
487 "F303BOOT", # STM32F303 Discovery |
|
488 ], |
|
489 }, |
|
490 "instructions": QCoreApplication.translate( |
|
491 "UF2FlashDialog", |
|
492 "<h3>CircuitPython Board</h3>" |
|
493 "<p>In order to prepare the board for flashing follow these" |
|
494 " steps:</p><ol>" |
|
495 "<li>Switch your device to 'bootloader' mode by double-pressing" |
|
496 " the reset button.</li>" |
|
497 "<li>Wait until the device has entered 'bootloader' mode.</li>" |
|
498 "<li>(If this does not happen, then try shorter or longer" |
|
499 " pauses between presses.)</li>" |
|
500 "<li>Ensure the boot volume is available (this may require" |
|
501 " mounting it).</li>" |
|
502 "<li>Select the firmware file to be flashed and click the" |
|
503 " flash button.</li>" |
|
504 "</ol>" |
|
505 ), |
|
506 "show_all": True, |
|
507 "firmware": "CircuitPython", |
|
508 }, |
|
509 |
|
510 "circuitpython_rp2040": { |
|
511 "volumes": { |
|
512 (0x239A, 0x80F4): [ |
|
513 "RPI-RP2", # Raspberry Pi Pico loaded with CircuitPython |
|
514 ], |
|
515 }, |
|
516 "instructions": QCoreApplication.translate( |
|
517 "UF2FlashDialog", |
|
518 "<h3>Pi Pico (RP2040) Board</h3>" |
|
519 "<p>In order to prepare the board for flashing follow these" |
|
520 " steps:</p><ol>" |
|
521 "<li>Enter 'bootloader' mode (board <b>without</b> RESET button):" |
|
522 "<ul>" |
|
523 "<li>Plug in your board while holding the BOOTSEL button.</li>" |
|
524 "</ul>" |
|
525 "Enter 'bootloader' mode (board <b>with</b> RESET button):" |
|
526 "<ul>" |
|
527 "<li>hold down RESET</li>" |
|
528 "<li>hold down BOOTSEL</li>" |
|
529 "<li>release RESET</li>" |
|
530 "<li>release BOOTSEL</li>" |
|
531 "</ul></li>" |
|
532 "<li>Wait until the device has entered 'bootloader' mode.</li>" |
|
533 "<li>Ensure the boot volume is available (this may require" |
|
534 " mounting it).</li>" |
|
535 "<li>Select the firmware file to be flashed and click the" |
|
536 " flash button.</li>" |
|
537 "</ol>" |
|
538 ), |
|
539 "show_all": False, |
|
540 "firmware": "CircuitPython", |
|
541 }, |
|
542 |
|
543 "rp2040": { |
|
544 "volumes": { |
|
545 (0x0000, 0x0000): [ |
|
546 "RPI-RP2", # Raspberry Pi Pico does not present a TTY |
|
547 ], |
|
548 }, |
|
549 "instructions": QCoreApplication.translate( |
|
550 "UF2FlashDialog", |
|
551 "<h3>Pi Pico (RP2040) Board</h3>" |
|
552 "<p>In order to prepare the board for flashing follow these" |
|
553 " steps:</p><ol>" |
|
554 "<li>Enter 'bootloader' mode (board <b>without</b> RESET button):" |
|
555 "<ul>" |
|
556 "<li>Plug in your board while holding the BOOTSEL button.</li>" |
|
557 "</ul>" |
|
558 "Enter 'bootloader' mode (board <b>with</b> RESET button):" |
|
559 "<ul>" |
|
560 "<li>hold down RESET</li>" |
|
561 "<li>hold down BOOTSEL</li>" |
|
562 "<li>release RESET</li>" |
|
563 "<li>release BOOTSEL</li>" |
|
564 "</ul></li>" |
|
565 "<li>Wait until the device has entered 'bootloader' mode.</li>" |
|
566 "<li>Ensure the boot volume is available (this may require" |
|
567 " mounting it).</li>" |
|
568 "<li>Select the firmware file to be flashed and click the" |
|
569 " flash button.</li>" |
|
570 "</ol>" |
|
571 ), |
|
572 "show_all": True, |
|
573 "firmware": "MicroPython / CircuitPython", |
|
574 }, |
|
575 } |
|
576 |
|
577 |
|
578 def getFoundDevices(boardType=""): |
|
579 """ |
|
580 Function to get the list of known serial devices supporting UF2. |
|
581 |
|
582 @param boardType specific board type to search for |
|
583 @type str |
|
584 @return list of tuples with the board type, the port description, the |
|
585 VID and PID |
|
586 @rtype list of tuple of (str, str, int, int) |
|
587 """ |
|
588 from PyQt6.QtSerialPort import QSerialPortInfo |
|
589 |
|
590 foundDevices = [] |
|
591 |
|
592 availablePorts = QSerialPortInfo.availablePorts() |
|
593 for port in availablePorts: |
|
594 vid = port.vendorIdentifier() |
|
595 pid = port.productIdentifier() |
|
596 |
|
597 if vid == 0 and pid == 0: |
|
598 # no device detected at port |
|
599 continue |
|
600 |
|
601 for board in SupportedUF2Boards: |
|
602 if ( |
|
603 (not boardType or (board.startswith(boardType))) and |
|
604 (vid, pid) in SupportedUF2Boards[board]["volumes"] |
|
605 ): |
|
606 foundDevices.append(( |
|
607 board, |
|
608 port.description(), |
|
609 (vid, pid), |
|
610 )) |
|
611 |
|
612 # second run for boards needing special treatment (e.g. RP2040) |
|
613 for board in SupportedUF2Boards: |
|
614 if not boardType or (board == boardType): |
|
615 with contextlib.suppress(KeyError): |
|
616 # find mounted volume |
|
617 volumes = SupportedUF2Boards[board]["volumes"][(0, 0)] |
|
618 foundVolumes = [] |
|
619 for volume in volumes: |
|
620 foundVolumes += Utilities.findVolume(volume, findAll=True) |
|
621 if foundVolumes: |
|
622 foundDevices.append(( |
|
623 board, |
|
624 QCoreApplication.translate( |
|
625 "UF2FlashDialog", "'{0}' Board").format(board), |
|
626 (0, 0), # VID/PID of (0, 0) is special |
|
627 )) |
|
628 |
|
629 return foundDevices |
|
630 |
|
631 |
|
632 class UF2FlashDialog(QDialog, Ui_UF2FlashDialog): |
|
633 """ |
|
634 Class implementing a dialog to flash any UF2 capable device. |
|
635 """ |
|
636 DeviceTypeRole = Qt.ItemDataRole.UserRole |
|
637 DeviceVidPidRole = Qt.ItemDataRole.UserRole + 1 |
|
638 |
|
639 def __init__(self, boardType="", parent=None): |
|
640 """ |
|
641 Constructor |
|
642 |
|
643 @param boardType specific board type to show the dialog for |
|
644 @type str |
|
645 @param parent reference to the parent widget (defaults to None) |
|
646 @type QWidget (optional) |
|
647 """ |
|
648 super().__init__(parent) |
|
649 self.setupUi(self) |
|
650 |
|
651 self.refreshButton.setIcon(UI.PixmapCache.getIcon("rescan")) |
|
652 |
|
653 self.firmwarePicker.setMode(EricPathPickerModes.OPEN_FILE_MODE) |
|
654 self.firmwarePicker.setFilters( |
|
655 self.tr("MicroPython/CircuitPython Files (*.uf2);;" |
|
656 "All Files (*)")) |
|
657 |
|
658 self.bootPicker.setMode(EricPathPickerModes.DIRECTORY_SHOW_FILES_MODE) |
|
659 self.bootPicker.setEnabled(False) |
|
660 |
|
661 self.__mandatoryStyleSheet = ( |
|
662 "QLineEdit {border: 2px solid; border-color: #dd8888}" |
|
663 if ericApp().usesDarkPalette() else |
|
664 "QLineEdit {border: 2px solid; border-color: #800000}" |
|
665 ) |
|
666 self.__manualType = "<manual>" |
|
667 |
|
668 self.__boardType = boardType |
|
669 |
|
670 self.__populate() |
|
671 |
|
672 self.__updateFlashButton() |
|
673 |
|
674 def __populate(self): |
|
675 """ |
|
676 Private method to (re-)populate the dialog. |
|
677 """ |
|
678 # save the currently selected device |
|
679 currentDevice = self.devicesComboBox.currentText() |
|
680 firmwareFile = self.firmwarePicker.text() |
|
681 |
|
682 # clear the entries first |
|
683 self.devicesComboBox.clear() |
|
684 self.firmwarePicker.clear() |
|
685 self.bootPicker.clear() |
|
686 self.infoLabel.clear() |
|
687 self.infoEdit.clear() |
|
688 |
|
689 # now populate the entries with data |
|
690 devices = getFoundDevices(boardType=self.__boardType) |
|
691 if len(devices) == 0: |
|
692 # no device detected |
|
693 devices = list(filter( |
|
694 lambda x: x[0] in SupportedUF2Boards, |
|
695 MicroPythonDevices.getFoundDevices()[0] |
|
696 )) |
|
697 if devices: |
|
698 self.__showSpecificInstructions(list(devices)) |
|
699 else: |
|
700 self.__showAllInstructions() |
|
701 self.devicesComboBox.addItem("") |
|
702 self.devicesComboBox.addItem(self.tr("Manual Select")) |
|
703 self.devicesComboBox.setItemData(1, self.__manualType, |
|
704 self.DeviceTypeRole) |
|
705 elif len(devices) == 1: |
|
706 # set the board type to the found one |
|
707 self.__boardType = devices[0][0] |
|
708 |
|
709 self.devicesComboBox.addItem(devices[0][1]) |
|
710 self.devicesComboBox.setItemData( |
|
711 0, devices[0][0], self.DeviceTypeRole) |
|
712 self.devicesComboBox.setItemData( |
|
713 0, devices[0][2], self.DeviceVidPidRole) |
|
714 self.devicesComboBox.addItem(self.tr("Manual Select")) |
|
715 self.devicesComboBox.setItemData(1, self.__manualType, |
|
716 self.DeviceTypeRole) |
|
717 self.on_devicesComboBox_currentIndexChanged(0) |
|
718 else: |
|
719 self.devicesComboBox.addItem("") |
|
720 for index, (boardType, description, |
|
721 vidpid) in enumerate(sorted(devices), 1): |
|
722 self.devicesComboBox.addItem(description) |
|
723 self.devicesComboBox.setItemData( |
|
724 index, boardType, self.DeviceTypeRole) |
|
725 self.devicesComboBox.setItemData( |
|
726 index, vidpid, self.DeviceVidPidRole) |
|
727 self.devicesComboBox.addItem(self.tr("Manual Select")) |
|
728 self.devicesComboBox.setItemData(index + 1, self.__manualType, |
|
729 self.DeviceTypeRole) |
|
730 |
|
731 # reselect the remembered device, if it is still there |
|
732 if currentDevice: |
|
733 self.devicesComboBox.setCurrentText(currentDevice) |
|
734 self.firmwarePicker.setText(firmwareFile) |
|
735 else: |
|
736 self.devicesComboBox.setCurrentIndex(0) |
|
737 |
|
738 def __updateFlashButton(self): |
|
739 """ |
|
740 Private method to update the state of the Flash button and the retest |
|
741 button. |
|
742 """ |
|
743 firmwareFile = self.firmwarePicker.text() |
|
744 if self.devicesComboBox.currentData(self.DeviceTypeRole) is not None: |
|
745 if bool(firmwareFile) and os.path.exists(firmwareFile): |
|
746 self.firmwarePicker.setStyleSheet("") |
|
747 else: |
|
748 self.firmwarePicker.setStyleSheet(self.__mandatoryStyleSheet) |
|
749 |
|
750 if bool(self.bootPicker.text()): |
|
751 self.bootPicker.setStyleSheet("") |
|
752 else: |
|
753 self.bootPicker.setStyleSheet(self.__mandatoryStyleSheet) |
|
754 else: |
|
755 self.firmwarePicker.setStyleSheet("") |
|
756 self.bootPicker.setStyleSheet("") |
|
757 |
|
758 enable = ( |
|
759 bool(self.bootPicker.text()) and |
|
760 bool(firmwareFile) and |
|
761 os.path.exists(firmwareFile) |
|
762 ) |
|
763 self.flashButton.setEnabled(enable) |
|
764 |
|
765 def __showAllInstructions(self): |
|
766 """ |
|
767 Private method to show instructions for resetting devices to bootloader |
|
768 mode. |
|
769 """ |
|
770 self.infoLabel.setText(self.tr("Reset Instructions:")) |
|
771 |
|
772 htmlText = self.tr( |
|
773 "<h4>No known devices detected.</h4>" |
|
774 "<p>Follow the appropriate instructions below to set <b>one</b>" |
|
775 " board into 'bootloader' mode. Press <b>Refresh</b> when ready." |
|
776 "</p>" |
|
777 ) |
|
778 for boardType in SupportedUF2Boards: |
|
779 if SupportedUF2Boards[boardType]["show_all"]: |
|
780 htmlText += ( |
|
781 "<hr/>" + |
|
782 SupportedUF2Boards[boardType]["instructions"] |
|
783 ) |
|
784 self.infoEdit.setHtml(htmlText) |
|
785 |
|
786 def __showSpecificInstructions(self, devices): |
|
787 """ |
|
788 Private method to show instructions for resetting devices to bootloader |
|
789 mode for a list of detected devices. |
|
790 |
|
791 @param devices list of detected devices |
|
792 @type list of str |
|
793 """ |
|
794 boardTypes = {x[0] for x in devices} |
|
795 |
|
796 self.infoLabel.setText(self.tr("Reset Instructions:")) |
|
797 |
|
798 if self.__boardType: |
|
799 htmlText = self.tr( |
|
800 "<h4>Flash {0} Firmware</h4>" |
|
801 "<p>Follow the instructions below to set <b>one</b> board into" |
|
802 " 'bootloader' mode. Press <b>Refresh</b> when ready.</p>" |
|
803 "<hr/>{1}" |
|
804 ).format( |
|
805 SupportedUF2Boards[self.__boardType]["firmware"], |
|
806 SupportedUF2Boards[self.__boardType]["instructions"], |
|
807 ) |
|
808 else: |
|
809 htmlText = self.tr( |
|
810 "<h4>Potentially UF2 capable devices found</h4>" |
|
811 "<p>Found these potentially UF2 capable devices:</p>" |
|
812 "<ul><li>{0}</li></ul>" |
|
813 "<p>Follow the instructions below to set <b>one</b> board into" |
|
814 " 'bootloader' mode. Press <b>Refresh</b> when ready.</p>" |
|
815 ).format( |
|
816 "</li><li>".join(sorted(x[1] for x in devices)) |
|
817 ) |
|
818 for boardType in sorted(boardTypes): |
|
819 htmlText += ( |
|
820 "<hr/>" + SupportedUF2Boards[boardType]["instructions"] |
|
821 ) |
|
822 self.infoEdit.setHtml(htmlText) |
|
823 |
|
824 def __showTypedInstructions(self, boardType): |
|
825 """ |
|
826 Private method to show instructions for resetting devices to bootloader |
|
827 mode for a specific board type. |
|
828 |
|
829 @param boardType type of the board to show instructions for |
|
830 @type str |
|
831 """ |
|
832 self.infoLabel.setText(self.tr("Reset Instructions:")) |
|
833 |
|
834 htmlText = self.tr( |
|
835 "<h4>No known devices detected.</h4>" |
|
836 "<p>Follow the instructions below to set <b>one</b> board into" |
|
837 " 'bootloader' mode. Press <b>Refresh</b> when ready.</p>" |
|
838 ) |
|
839 htmlText += "<hr/>" + SupportedUF2Boards[boardType]["instructions"] |
|
840 self.infoEdit.setHtml(htmlText) |
|
841 |
|
842 def __showManualInstructions(self): |
|
843 """ |
|
844 Private method to show instructions for flashing devices manually. |
|
845 """ |
|
846 self.infoLabel.setText(self.tr("Flash Instructions:")) |
|
847 |
|
848 htmlText = self.tr( |
|
849 "<h4>Flash method 'manual' selected.</h4>" |
|
850 "<p>Follow the instructions below to flash a device by entering" |
|
851 " the data manually.</p><ol>" |
|
852 "<li>Change the device to 'bootloader' mode.</li>" |
|
853 "<li>Wait until the device has entered 'bootloader' mode.</li>" |
|
854 "<li>Ensure the boot volume is available (this may require" |
|
855 " mounting it) and select its path.</li>" |
|
856 "<li>Select the firmware file to be flashed and click the" |
|
857 " flash button.</li>" |
|
858 "</ol>" |
|
859 ) |
|
860 for boardType in SupportedUF2Boards: |
|
861 htmlText += "<hr/>" + SupportedUF2Boards[boardType]["instructions"] |
|
862 self.infoEdit.setHtml(htmlText) |
|
863 |
|
864 def __showNoVolumeInformation(self, volumes, boardType): |
|
865 """ |
|
866 Private method to show information about the expected boot volume(s). |
|
867 |
|
868 @param volumes list of expected volume names |
|
869 @type list of str |
|
870 @param boardType type of the board to show instructions for |
|
871 @type str |
|
872 """ |
|
873 self.infoLabel.setText(self.tr("Boot Volume not found:")) |
|
874 |
|
875 htmlText = self.tr( |
|
876 "<h4>No Boot Volume detected.</h4>" |
|
877 "<p>Please ensure that the boot volume of the device to be flashed" |
|
878 " is available. " |
|
879 ) |
|
880 if len(volumes) == 1: |
|
881 htmlText += self.tr( |
|
882 "This volume should be named <b>{0}</b>." |
|
883 " Press <b>Refresh</b> when ready.</p>" |
|
884 ).format(volumes[0]) |
|
885 else: |
|
886 htmlText += self.tr( |
|
887 "This volume should have one of these names.</p>" |
|
888 "<ul><li>{0}</li></ul>" |
|
889 "<p>Press <b>Refresh</b> when ready.</p>" |
|
890 ).format("</li><li>".join(sorted(volumes))) |
|
891 |
|
892 if boardType: |
|
893 htmlText += self.tr( |
|
894 "<h4>Reset Instructions</h4>" |
|
895 "<p>Follow the instructions below to set the board into" |
|
896 " 'bootloader' mode. Press <b>Refresh</b> when ready.</p>" |
|
897 ) |
|
898 htmlText += "<hr/>" + SupportedUF2Boards[boardType]["instructions"] |
|
899 |
|
900 self.infoEdit.setHtml(htmlText) |
|
901 |
|
902 def __showMultipleVolumesInformation(self, volumePaths): |
|
903 """ |
|
904 Private method to show information because multiple devices of the |
|
905 same type are ready for flashing. |
|
906 |
|
907 Note: This is a dangerous situation! |
|
908 |
|
909 @param volumePaths list of volume paths |
|
910 @type list of str |
|
911 """ |
|
912 self.infoLabel.setText(self.tr("Multiple Boot Volumes found:")) |
|
913 |
|
914 htmlText = self.tr( |
|
915 "<h4>Multiple Boot Volumes were found</h4>" |
|
916 "<p>These volume paths were found.</p><ul><li>{0}</li></ul>" |
|
917 "<p>Please ensure that only one device of a type is ready for" |
|
918 " flashing. Press <b>Refresh</b> when ready.</p>" |
|
919 ).format("</li><li>".join(sorted(volumePaths))) |
|
920 self.infoEdit.setHtml(htmlText) |
|
921 |
|
922 @pyqtSlot() |
|
923 def on_flashButton_clicked(self): |
|
924 """ |
|
925 Private slot to flash the selected MicroPython or CircuitPython |
|
926 firmware onto the device. |
|
927 """ |
|
928 boardType = self.devicesComboBox.currentData(self.DeviceTypeRole) |
|
929 firmwarePath = self.firmwarePicker.text() |
|
930 volumePath = self.bootPicker.text() |
|
931 if os.path.exists(firmwarePath) and os.path.exists(volumePath): |
|
932 if boardType == self.__manualType: |
|
933 self.infoLabel.setText(self.tr("Flashing Firmware")) |
|
934 self.infoEdit.setHtml(self.tr( |
|
935 "<p>Flashing the selected firmware to the device. Please" |
|
936 " wait until the device resets automatically.</p>") |
|
937 ) |
|
938 else: |
|
939 firmwareType = SupportedUF2Boards[boardType]["firmware"] |
|
940 self.infoLabel.setText( |
|
941 self.tr("Flashing {0}").format(firmwareType)) |
|
942 self.infoEdit.setHtml(self.tr( |
|
943 "<p>Flashing the {0} firmware to the device. Please wait" |
|
944 " until the device resets automatically.</p>" |
|
945 ).format(firmwareType)) |
|
946 QCoreApplication.processEvents( |
|
947 QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents) |
|
948 shutil.copy2(firmwarePath, volumePath) |
|
949 QThread.sleep(1) |
|
950 self.on_refreshButton_clicked() |
|
951 |
|
952 @pyqtSlot() |
|
953 def on_refreshButton_clicked(self): |
|
954 """ |
|
955 Private slot to refresh the dialog. |
|
956 """ |
|
957 # special treatment for RPi Pico |
|
958 if self.__boardType == "circuitpython_rp2040": |
|
959 self.__boardType = "rp2040" |
|
960 |
|
961 self.__populate() |
|
962 |
|
963 @pyqtSlot(int) |
|
964 def on_devicesComboBox_currentIndexChanged(self, index): |
|
965 """ |
|
966 Private slot to handle the selection of a board. |
|
967 |
|
968 @param index selected index |
|
969 @type int |
|
970 """ |
|
971 vidpid = self.devicesComboBox.itemData(index, self.DeviceVidPidRole) |
|
972 boardType = self.devicesComboBox.itemData(index, self.DeviceTypeRole) |
|
973 |
|
974 self.bootPicker.setEnabled(boardType == self.__manualType) |
|
975 if boardType == self.__manualType: |
|
976 self.__showManualInstructions() |
|
977 |
|
978 if vidpid is None: |
|
979 if boardType is None: |
|
980 self.bootPicker.clear() |
|
981 else: |
|
982 volumes = SupportedUF2Boards[boardType]["volumes"][vidpid] |
|
983 foundVolumes = [] |
|
984 for volume in volumes: |
|
985 foundVolumes += Utilities.findVolume(volume, findAll=True) |
|
986 |
|
987 if len(foundVolumes) == 0: |
|
988 self.__showNoVolumeInformation(volumes, boardType) |
|
989 self.bootPicker.clear() |
|
990 elif len(foundVolumes) == 1: |
|
991 self.bootPicker.setText(foundVolumes[0]) |
|
992 else: |
|
993 self.__showMultipleVolumesInformation() |
|
994 self.bootPicker.clear() |
|
995 |
|
996 self.__updateFlashButton() |
|
997 |
|
998 @pyqtSlot(str) |
|
999 def on_firmwarePicker_textChanged(self, text): |
|
1000 """ |
|
1001 Private slot handling a change of the firmware file. |
|
1002 |
|
1003 @param text current text of the firmware edit |
|
1004 @type str |
|
1005 """ |
|
1006 self.__updateFlashButton() |
|
1007 |
|
1008 @pyqtSlot(str) |
|
1009 def on_bootPicker_textChanged(self, text): |
|
1010 """ |
|
1011 Private slot handling a change of the boot volume. |
|
1012 |
|
1013 @param text current text of the boot volume edit |
|
1014 @type str |
|
1015 """ |
|
1016 self.__updateFlashButton() |