|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2023 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a dialog to convert a .hex or .bin firmware file to .uf2. |
|
8 """ |
|
9 |
|
10 import json |
|
11 import os |
|
12 |
|
13 from PyQt6.QtCore import QProcess, QRegularExpression, pyqtSlot |
|
14 from PyQt6.QtGui import QRegularExpressionValidator |
|
15 from PyQt6.QtWidgets import QDialog |
|
16 |
|
17 from eric7 import Preferences |
|
18 from eric7.EricWidgets.EricPathPicker import EricPathPickerModes |
|
19 from eric7.SystemUtilities.PythonUtilities import getPythonExecutable |
|
20 |
|
21 from .Ui_ConvertToUF2Dialog import Ui_ConvertToUF2Dialog |
|
22 |
|
23 |
|
24 class ConvertToUF2Dialog(QDialog, Ui_ConvertToUF2Dialog): |
|
25 """ |
|
26 Class implementing a dialog to convert a .hex or .bin firmware file to .uf2. |
|
27 """ |
|
28 |
|
29 FamiliesFile = os.path.join(os.path.dirname(__file__), "Tools", "uf2families.json") |
|
30 ConvertScript = os.path.join(os.path.dirname(__file__), "Tools", "uf2conv.py") |
|
31 |
|
32 def __init__(self, parent=None): |
|
33 """ |
|
34 Constructor |
|
35 |
|
36 @param parent reference to the parent widget (defaults to None) |
|
37 @type QWidget (optional) |
|
38 """ |
|
39 super().__init__(parent) |
|
40 self.setupUi(self) |
|
41 |
|
42 self.firmwarePicker.setMode(EricPathPickerModes.OPEN_FILE_MODE) |
|
43 self.firmwarePicker.setFilters( |
|
44 self.tr("MicroPython Firmware Files (*.hex *.bin);;All Files (*)") |
|
45 ) |
|
46 |
|
47 self.__validator = QRegularExpressionValidator( |
|
48 QRegularExpression(r"[0-9a-fA-F]{0,7}") |
|
49 ) |
|
50 self.addressEdit.setValidator(self.__validator) |
|
51 self.addressEdit.setEnabled(False) |
|
52 |
|
53 self.__populateFamilyComboBox() |
|
54 |
|
55 self.__process = QProcess(self) |
|
56 self.__process.readyReadStandardOutput.connect(self.__readOutput) |
|
57 self.__process.readyReadStandardError.connect(self.__readError) |
|
58 self.__process.finished.connect(self.__conversionFinished) |
|
59 |
|
60 self.__updateConvertButton() |
|
61 |
|
62 def __populateFamilyComboBox(self): |
|
63 """ |
|
64 Private method to populate the chip family combo box with values read from |
|
65 'uf2families.json' file. |
|
66 """ |
|
67 with open(ConvertToUF2Dialog.FamiliesFile, "r") as f: |
|
68 families = json.load(f) |
|
69 |
|
70 self.familiesComboBox.addItem("", "") |
|
71 for family in families: |
|
72 self.familiesComboBox.addItem(family["description"], family["id"]) |
|
73 self.familiesComboBox.model().sort(0) |
|
74 |
|
75 def __updateConvertButton(self): |
|
76 """ |
|
77 Private method to set the enabled status of the 'Convert' button. |
|
78 """ |
|
79 self.convertButton.setEnabled( |
|
80 bool(self.firmwarePicker.text()) |
|
81 and bool(self.familiesComboBox.currentText()) |
|
82 ) |
|
83 |
|
84 @pyqtSlot(str) |
|
85 def on_firmwarePicker_textChanged(self, firmware): |
|
86 """ |
|
87 Private slot handling a change of the firmware file name. |
|
88 |
|
89 @param firmware name of the firmware file |
|
90 @type str |
|
91 """ |
|
92 self.addressEdit.setEnabled(firmware.lower().endswith(".bin")) |
|
93 self.__updateConvertButton() |
|
94 |
|
95 @pyqtSlot(str) |
|
96 def on_familiesComboBox_currentTextChanged(self, family): |
|
97 """ |
|
98 Private slot handling the selection of a chip family. |
|
99 |
|
100 @param family name of the selected chip family |
|
101 @type str |
|
102 """ |
|
103 self.__updateConvertButton() |
|
104 |
|
105 @pyqtSlot() |
|
106 def on_convertButton_clicked(self): |
|
107 """ |
|
108 Private slot activating the conversion process. |
|
109 """ |
|
110 self.outputEdit.clear() |
|
111 |
|
112 inputFile = self.firmwarePicker.text() |
|
113 outputFile = os.path.splitext(inputFile)[0] + ".uf2" |
|
114 args = [ |
|
115 ConvertToUF2Dialog.ConvertScript, |
|
116 "--convert", |
|
117 "--family", |
|
118 self.familiesComboBox.currentData(), |
|
119 "--output", |
|
120 outputFile, |
|
121 ] |
|
122 if inputFile.lower().endswith(".bin"): |
|
123 address = self.addressEdit.text() |
|
124 if address: |
|
125 args.extend(["--base", "0x{0}".format(address)]) |
|
126 args.append(inputFile) |
|
127 python = getPythonExecutable() |
|
128 |
|
129 # output the generated command |
|
130 self.outputEdit.insertPlainText( |
|
131 "{0} {1}\n{2}\n\n".format(python, " ".join(args), "=" * 40) |
|
132 ) |
|
133 self.outputEdit.ensureCursorVisible() |
|
134 |
|
135 # start the conversion process |
|
136 self.convertButton.setEnabled(False) |
|
137 self.__process.start(python, args) |
|
138 |
|
139 @pyqtSlot() |
|
140 def __readOutput(self): |
|
141 """ |
|
142 Private slot to read the standard output channel of the conversion process. |
|
143 """ |
|
144 out = str( |
|
145 self.__process.readAllStandardOutput(), |
|
146 Preferences.getSystem("IOEncoding"), |
|
147 "replace", |
|
148 ) |
|
149 self.outputEdit.insertPlainText(out) |
|
150 self.outputEdit.ensureCursorVisible() |
|
151 |
|
152 @pyqtSlot() |
|
153 def __readError(self): |
|
154 """ |
|
155 Private slot to read the standard error channel of the conversion process. |
|
156 """ |
|
157 out = str( |
|
158 self.__process.readAllStandardError(), |
|
159 Preferences.getSystem("IOEncoding"), |
|
160 "replace", |
|
161 ) |
|
162 self.outputEdit.insertPlainText(self.tr("--- ERROR ---\n")) |
|
163 self.outputEdit.insertPlainText(out) |
|
164 self.outputEdit.ensureCursorVisible() |
|
165 |
|
166 def __conversionFinished(self, exitCode, exitStatus): |
|
167 """ |
|
168 Private slot handling the end of the conversion process. |
|
169 |
|
170 @param exitCode exit code of the process |
|
171 @type int |
|
172 @param exitStatus exit status of the process |
|
173 @type QProcess.ExitStatus |
|
174 """ |
|
175 self.convertButton.setEnabled(True) |