10 import contextlib |
10 import contextlib |
11 import os |
11 import os |
12 import shutil |
12 import shutil |
13 |
13 |
14 from PyQt6.QtCore import QCoreApplication, QEventLoop, Qt, QThread, pyqtSlot |
14 from PyQt6.QtCore import QCoreApplication, QEventLoop, Qt, QThread, pyqtSlot |
15 from PyQt6.QtSerialPort import QSerialPortInfo |
|
16 from PyQt6.QtWidgets import QDialog, QInputDialog |
15 from PyQt6.QtWidgets import QDialog, QInputDialog |
17 |
16 |
18 from eric7.EricGui import EricPixmapCache |
17 from eric7.EricGui import EricPixmapCache |
19 from eric7.EricWidgets import EricMessageBox |
18 from eric7.EricWidgets import EricMessageBox |
20 from eric7.EricWidgets.EricApplication import ericApp |
19 from eric7.EricWidgets.EricApplication import ericApp |
26 |
25 |
27 with contextlib.suppress(ImportError): |
26 with contextlib.suppress(ImportError): |
28 import usb.core |
27 import usb.core |
29 |
28 |
30 SupportedUF2Boards = { |
29 SupportedUF2Boards = { |
31 "circuitpython": { |
30 "MPy or CPy": { |
32 "volumes": { |
31 "volumes": { |
33 (0x03EB, 0x2402): [ |
32 (0x03EB, 0x2402): [ |
34 ("SAMD21", "SAMD21 Board"), |
33 ("SAMD21", "SAMD21 Board"), |
35 ("SAME54", "SAME54 Board"), |
34 ("SAME54", "SAME54 Board"), |
36 ], |
35 ], |
776 "<li>Select the firmware file to be flashed and click the" |
775 "<li>Select the firmware file to be flashed and click the" |
777 " flash button.</li>" |
776 " flash button.</li>" |
778 "</ol>", |
777 "</ol>", |
779 ), |
778 ), |
780 "show_all": True, |
779 "show_all": True, |
781 "firmware": "CircuitPython", |
|
782 }, |
|
783 "nrf52": { |
|
784 "volumes": { |
|
785 (0x239A, 0x0029): [ |
|
786 ("FTHR840BOOT", "Feather nRF52840 Express"), |
|
787 ("MDK840DONGL", "MDK nRF52840 USB Dongle"), |
|
788 ], |
|
789 (0x239A, 0x0063): [ |
|
790 ("NANO33BOOT", "Nano 33 BLE"), |
|
791 ], |
|
792 (0x239A, 0x00DA): [ |
|
793 ("XENONBOOT ", "Particle Xenon"), |
|
794 ], |
|
795 (0x2886, 0x0045): [ |
|
796 ("XIAO-SENSE", "XIAO nRF52840 Sense"), |
|
797 ], |
|
798 }, |
|
799 "instructions": QCoreApplication.translate( |
|
800 "UF2FlashDialog", |
|
801 "<h3>NRF52 Board</h3>" |
|
802 "<p>In order to prepare the board for flashing follow these" |
|
803 " steps:</p><ol>" |
|
804 "<li>Switch your device to 'bootloader' mode by double-pressing" |
|
805 " the reset button.</li>" |
|
806 "<li>Wait until the device has entered 'bootloader' mode.</li>" |
|
807 "<li>(If this does not happen, then try shorter or longer" |
|
808 " pauses between presses.)</li>" |
|
809 "<li>Ensure the boot volume is available (this may require" |
|
810 " mounting it).</li>" |
|
811 "<li>Select the firmware file to be flashed and click the" |
|
812 " flash button.</li>" |
|
813 "</ol>", |
|
814 ), |
|
815 "show_all": True, |
|
816 "firmware": "MicroPython / CircuitPython", |
780 "firmware": "MicroPython / CircuitPython", |
817 }, |
781 }, |
818 "rp2": { |
782 "RP2": { |
819 "volumes": { |
783 "volumes": { |
820 (0x0000, 0x0000): [ |
784 (0x2E8A, 0x0003): [ |
821 ("RPI-RP2", "Raspberry Pi Pico"), # we don't have valid VID/PID |
785 ("RPI-RP2", "Raspberry Pi Pico"), |
822 ("RP2350", "Raspberry Pi Pico 2"), # we don't have valid VID/PID |
786 ], |
|
787 (0x2E8A, 0x000F): [ |
|
788 ("RP2350", "Raspberry Pi Pico 2"), |
823 ], |
789 ], |
824 }, |
790 }, |
825 "instructions": QCoreApplication.translate( |
791 "instructions": QCoreApplication.translate( |
826 "UF2FlashDialog", |
792 "UF2FlashDialog", |
827 "<h3>Pi Pico (RP2040/RP2350) Board</h3>" |
793 "<h3>Pi Pico (RP2040/RP2350) Board</h3>" |
891 for board, description, vidpid in foundDevices.copy(): |
857 for board, description, vidpid in foundDevices.copy(): |
892 if not usb.core.find(idVendor=vidpid[0], idProduct=vidpid[1]): |
858 if not usb.core.find(idVendor=vidpid[0], idProduct=vidpid[1]): |
893 foundDevices.discard((board, description, vidpid)) |
859 foundDevices.discard((board, description, vidpid)) |
894 |
860 |
895 # step 2: determine all devices that have their UF2 volume not mounted |
861 # step 2: determine all devices that have their UF2 volume not mounted |
896 availablePorts = QSerialPortInfo.availablePorts() |
862 for device in usb.core.find(find_all=True): |
897 for port in availablePorts: |
863 vid = device.idVendor |
898 vid = port.vendorIdentifier() |
864 pid = device.idProduct |
899 pid = port.productIdentifier() |
|
900 |
|
901 if vid == 0 and pid == 0: |
|
902 # no device detected at port |
|
903 continue |
|
904 |
865 |
905 for board in SupportedUF2Boards: |
866 for board in SupportedUF2Boards: |
906 if (not boardType or (board.startswith(boardType))) and ( |
867 if (not boardType or (board.startswith(boardType))) and ( |
907 vid, |
868 vid, |
908 pid, |
869 pid, |
909 ) in SupportedUF2Boards[board]["volumes"]: |
870 ) in SupportedUF2Boards[board]["volumes"]: |
910 for device in foundDevices: |
871 for device in foundDevices: |
911 if (vid, pid) == device[2]: |
872 if (vid, pid) == device[2]: |
912 break |
873 break |
913 else: |
874 else: |
|
875 description = ", ".join( |
|
876 v[1] for v in SupportedUF2Boards[board]["volumes"][(vid, pid)] |
|
877 ) |
914 foundDevices.add( |
878 foundDevices.add( |
915 ( |
879 ( |
916 board, |
880 board, |
917 port.description(), |
881 description, |
918 (vid, pid), |
882 (vid, pid), |
919 ) |
883 ) |
920 ) |
884 ) |
921 |
885 |
922 return [*foundDevices] |
886 return [*foundDevices] |
1207 "<h4>Multiple Boot Volumes were found</h4>" |
1171 "<h4>Multiple Boot Volumes were found</h4>" |
1208 "<p>These volume paths were found.</p><ul><li>{0}</li></ul>" |
1172 "<p>These volume paths were found.</p><ul><li>{0}</li></ul>" |
1209 "<p>Please ensure that only one device of a type is ready for" |
1173 "<p>Please ensure that only one device of a type is ready for" |
1210 " flashing. Press <b>Refresh</b> when ready.</p>" |
1174 " flashing. Press <b>Refresh</b> when ready.</p>" |
1211 ).format("</li><li>".join(sorted(volumePaths))) |
1175 ).format("</li><li>".join(sorted(volumePaths))) |
|
1176 self.infoEdit.setHtml(htmlText) |
|
1177 |
|
1178 def __showFlashInstruction(self): |
|
1179 """ |
|
1180 Private method to show information for the actual flashing process. |
|
1181 """ |
|
1182 self.infoLabel.setText(self.tr("Flash Instructions:")) |
|
1183 |
|
1184 htmlText = self.tr( |
|
1185 "<h4>Flash selected device.</h4>" |
|
1186 "<p>Follow the instructions below to flash the selected device.</p><ol>" |
|
1187 "<li>Select the firmware file to be flashed.</li>" |
|
1188 "<li>Click the flash button.</li>" |
|
1189 "</ol>" |
|
1190 ) |
1212 self.infoEdit.setHtml(htmlText) |
1191 self.infoEdit.setHtml(htmlText) |
1213 |
1192 |
1214 @pyqtSlot() |
1193 @pyqtSlot() |
1215 def on_flashButton_clicked(self): |
1194 def on_flashButton_clicked(self): |
1216 """ |
1195 """ |
1251 """ |
1230 """ |
1252 Private slot to refresh the dialog. |
1231 Private slot to refresh the dialog. |
1253 """ |
1232 """ |
1254 self.__populate() |
1233 self.__populate() |
1255 self.__updateFlashButton() |
1234 self.__updateFlashButton() |
1256 self.on_devicesComboBox_currentIndexChanged(0) |
|
1257 |
1235 |
1258 @pyqtSlot(int) |
1236 @pyqtSlot(int) |
1259 def on_devicesComboBox_currentIndexChanged(self, index): |
1237 def on_devicesComboBox_currentIndexChanged(self, index): |
1260 """ |
1238 """ |
1261 Private slot to handle the selection of a board. |
1239 Private slot to handle the selection of a board. |
1276 self.bootPicker.clear() |
1254 self.bootPicker.clear() |
1277 else: |
1255 else: |
1278 volumes = SupportedUF2Boards[boardType]["volumes"][vidpid] |
1256 volumes = SupportedUF2Boards[boardType]["volumes"][vidpid] |
1279 foundVolumes = [] |
1257 foundVolumes = [] |
1280 for volume, _ in volumes: |
1258 for volume, _ in volumes: |
1281 foundVolumes += FileSystemUtilities.findVolume( |
1259 foundVolumes += FileSystemUtilities.findVolume(volume, findAll=True) |
1282 volume, findAll=True, markerFile="INFO_UF2.TXT" |
|
1283 ) |
|
1284 foundVolumes = list(set(foundVolumes)) # make entries unique |
1260 foundVolumes = list(set(foundVolumes)) # make entries unique |
1285 |
1261 |
1286 if len(foundVolumes) == 0: |
1262 if len(foundVolumes) == 0: |
1287 self.__showNoVolumeInformation([v[0] for v in volumes], boardType) |
1263 self.__showNoVolumeInformation([v[0] for v in volumes], boardType) |
1288 self.bootPicker.clear() |
1264 self.bootPicker.clear() |
1289 elif len(foundVolumes) == 1: |
1265 elif len(foundVolumes) == 1: |
|
1266 self.__showFlashInstruction() |
1290 self.bootPicker.setText(foundVolumes[0]) |
1267 self.bootPicker.setText(foundVolumes[0]) |
1291 else: |
1268 else: |
1292 self.__showMultipleVolumesInformation(foundVolumes) |
1269 self.__showMultipleVolumesInformation(foundVolumes) |
1293 self.bootPicker.clear() |
1270 self.bootPicker.clear() |
1294 |
1271 |