5 |
5 |
6 """ |
6 """ |
7 Module implementing a dialog to flash any UF2 capable device. |
7 Module implementing a dialog to flash any UF2 capable device. |
8 """ |
8 """ |
9 |
9 |
10 import contextlib |
|
11 import os |
10 import os |
12 import shutil |
11 import shutil |
13 |
12 |
14 from PyQt6.QtCore import QCoreApplication, QEventLoop, Qt, QThread, pyqtSlot |
13 from PyQt6.QtCore import QCoreApplication, QEventLoop, Qt, QThread, pyqtSlot |
15 from PyQt6.QtSerialPort import QSerialPortInfo |
14 from PyQt6.QtSerialPort import QSerialPortInfo |
578 "<li>Select the firmware file to be flashed and click the" |
577 "<li>Select the firmware file to be flashed and click the" |
579 " flash button.</li>" |
578 " flash button.</li>" |
580 "</ol>", |
579 "</ol>", |
581 ), |
580 ), |
582 "show_all": True, |
581 "show_all": True, |
583 "firmware": "CircuitPython", |
|
584 }, |
|
585 "circuitpython_rp2040": { |
|
586 "volumes": { |
|
587 (0x239A, 0x80F4): [ |
|
588 ("RPI-RP2", "Raspberry Pi Pico"), |
|
589 ], |
|
590 }, |
|
591 "instructions": QCoreApplication.translate( |
|
592 "UF2FlashDialog", |
|
593 "<h3>Pi Pico (RP2040) Board</h3>" |
|
594 "<p>In order to prepare the board for flashing follow these" |
|
595 " steps:</p><ol>" |
|
596 "<li>Enter 'bootloader' mode (board <b>without</b> RESET button):" |
|
597 "<ul>" |
|
598 "<li>Plug in your board while holding the BOOTSEL button.</li>" |
|
599 "</ul>" |
|
600 "Enter 'bootloader' mode (board <b>with</b> RESET button):" |
|
601 "<ul>" |
|
602 "<li>hold down RESET</li>" |
|
603 "<li>hold down BOOTSEL</li>" |
|
604 "<li>release RESET</li>" |
|
605 "<li>release BOOTSEL</li>" |
|
606 "</ul></li>" |
|
607 "<li>Wait until the device has entered 'bootloader' mode.</li>" |
|
608 "<li>Ensure the boot volume is available (this may require" |
|
609 " mounting it).</li>" |
|
610 "<li>Select the firmware file to be flashed and click the" |
|
611 " flash button.</li>" |
|
612 "</ol>", |
|
613 ), |
|
614 "show_all": False, |
|
615 "firmware": "CircuitPython", |
582 "firmware": "CircuitPython", |
616 }, |
583 }, |
617 "rp2040": { |
584 "rp2040": { |
618 "volumes": { |
585 "volumes": { |
619 (0x0000, 0x0000): [ |
586 (0x0000, 0x0000): [ |
659 VID and PID |
626 VID and PID |
660 @rtype list of tuple of (str, str, (int, int)) |
627 @rtype list of tuple of (str, str, (int, int)) |
661 """ |
628 """ |
662 foundDevices = set() |
629 foundDevices = set() |
663 |
630 |
|
631 # step 1: determine all known UF2 volumes that are mounted |
|
632 boardTypes = [boardType] if boardType else list(SupportedUF2Boards.keys()) |
|
633 for board in boardTypes: |
|
634 for vidpid, volumes in SupportedUF2Boards[board]["volumes"].items(): |
|
635 for volume, description in volumes: |
|
636 if FileSystemUtilities.findVolume(volume, findAll=True): |
|
637 foundDevices.add((board, description, vidpid)) |
|
638 |
|
639 # set 2: determine all devices that have their UF2 volume not mounted |
664 availablePorts = QSerialPortInfo.availablePorts() |
640 availablePorts = QSerialPortInfo.availablePorts() |
665 for port in availablePorts: |
641 for port in availablePorts: |
666 vid = port.vendorIdentifier() |
642 vid = port.vendorIdentifier() |
667 pid = port.productIdentifier() |
643 pid = port.productIdentifier() |
668 |
644 |
673 for board in SupportedUF2Boards: |
649 for board in SupportedUF2Boards: |
674 if (not boardType or (board.startswith(boardType))) and ( |
650 if (not boardType or (board.startswith(boardType))) and ( |
675 vid, |
651 vid, |
676 pid, |
652 pid, |
677 ) in SupportedUF2Boards[board]["volumes"]: |
653 ) in SupportedUF2Boards[board]["volumes"]: |
678 foundDevices.add( |
654 for device in foundDevices: |
679 ( |
655 if (vid, pid) == device[2]: |
680 board, |
656 break |
681 port.description(), |
657 else: |
682 (vid, pid), |
658 foundDevices.add( |
|
659 ( |
|
660 board, |
|
661 port.description(), |
|
662 (vid, pid), |
|
663 ) |
683 ) |
664 ) |
684 ) |
|
685 |
|
686 # second run for boards needing special treatment (e.g. RP2040) |
|
687 for board in SupportedUF2Boards: |
|
688 if not boardType or (board == boardType): |
|
689 with contextlib.suppress(KeyError): |
|
690 # find mounted volume |
|
691 volumes = SupportedUF2Boards[board]["volumes"][(0, 0)] |
|
692 for volume, description in volumes: |
|
693 if FileSystemUtilities.findVolume(volume, findAll=True): |
|
694 foundDevices.add( |
|
695 (board, port.description() or description, (0, 0)) |
|
696 ) |
|
697 |
|
698 # third run for CircuitPython boards in UF2 mode but with VID/PID being invalid |
|
699 for vidpid, volumes in SupportedUF2Boards["circuitpython"]["volumes"].items(): |
|
700 for volume, description in volumes: |
|
701 if FileSystemUtilities.findVolume(volume, findAll=True): |
|
702 foundDevices.add( |
|
703 ( |
|
704 "circuitpython", |
|
705 port.description() or description, |
|
706 vidpid, |
|
707 ) |
|
708 ) |
|
709 |
665 |
710 return list(foundDevices) |
666 return list(foundDevices) |
711 |
667 |
712 |
668 |
713 class UF2FlashDialog(QDialog, Ui_UF2FlashDialog): |
669 class UF2FlashDialog(QDialog, Ui_UF2FlashDialog): |
808 ) |
763 ) |
809 |
764 |
810 # select the remembered device, if it is still there |
765 # select the remembered device, if it is still there |
811 if currentDevice: |
766 if currentDevice: |
812 self.devicesComboBox.setCurrentText(currentDevice) |
767 self.devicesComboBox.setCurrentText(currentDevice) |
|
768 if self.devicesComboBox.currentIndex() == -1 and len(devices) == 1: |
|
769 # the device name has changed but only one is present; select it |
|
770 self.devicesComboBox.setCurrentIndex(0) |
813 self.firmwarePicker.setText(firmwareFile) |
771 self.firmwarePicker.setText(firmwareFile) |
814 elif len(devices) == 1: |
772 elif len(devices) == 1: |
815 self.devicesComboBox.setCurrentIndex(0) |
773 self.devicesComboBox.setCurrentIndex(0) |
816 else: |
774 else: |
817 self.devicesComboBox.setCurrentIndex(-1) |
775 self.devicesComboBox.setCurrentIndex(-1) |
1030 @pyqtSlot() |
988 @pyqtSlot() |
1031 def on_refreshButton_clicked(self): |
989 def on_refreshButton_clicked(self): |
1032 """ |
990 """ |
1033 Private slot to refresh the dialog. |
991 Private slot to refresh the dialog. |
1034 """ |
992 """ |
1035 # special treatment for RPi Pico |
|
1036 if self.__boardType == "circuitpython_rp2040": |
|
1037 self.__boardType = "rp2040" |
|
1038 |
|
1039 self.__populate() |
993 self.__populate() |
|
994 self.__updateFlashButton() |
1040 |
995 |
1041 @pyqtSlot(int) |
996 @pyqtSlot(int) |
1042 def on_devicesComboBox_currentIndexChanged(self, index): |
997 def on_devicesComboBox_currentIndexChanged(self, index): |
1043 """ |
998 """ |
1044 Private slot to handle the selection of a board. |
999 Private slot to handle the selection of a board. |
1067 self.__showNoVolumeInformation([v[0] for v in volumes], boardType) |
1022 self.__showNoVolumeInformation([v[0] for v in volumes], boardType) |
1068 self.bootPicker.clear() |
1023 self.bootPicker.clear() |
1069 elif len(foundVolumes) == 1: |
1024 elif len(foundVolumes) == 1: |
1070 self.bootPicker.setText(foundVolumes[0]) |
1025 self.bootPicker.setText(foundVolumes[0]) |
1071 else: |
1026 else: |
1072 self.__showMultipleVolumesInformation() |
1027 self.__showMultipleVolumesInformation(foundVolumes) |
1073 self.bootPicker.clear() |
1028 self.bootPicker.clear() |
1074 |
1029 |
1075 self.__updateFlashButton() |
1030 self.__updateFlashButton() |
1076 |
1031 |
1077 @pyqtSlot(str) |
1032 @pyqtSlot(str) |