1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2019 - 2023 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing the device interface class for ESP32 and ESP8266 based |
|
8 boards. |
|
9 """ |
|
10 |
|
11 from PyQt6.QtCore import QProcess, QUrl, pyqtSlot |
|
12 from PyQt6.QtNetwork import QNetworkRequest |
|
13 from PyQt6.QtWidgets import QDialog, QMenu |
|
14 |
|
15 from eric7 import Globals, Preferences |
|
16 from eric7.EricWidgets import EricMessageBox |
|
17 from eric7.EricWidgets.EricApplication import ericApp |
|
18 from eric7.EricWidgets.EricProcessDialog import EricProcessDialog |
|
19 from eric7.SystemUtilities import PythonUtilities |
|
20 |
|
21 from .MicroPythonDevices import FirmwareGithubUrls, MicroPythonDevice |
|
22 from .MicroPythonWidget import HAS_QTCHART |
|
23 |
|
24 |
|
25 class EspDevice(MicroPythonDevice): |
|
26 """ |
|
27 Class implementing the device for ESP32 and ESP8266 based boards. |
|
28 """ |
|
29 |
|
30 def __init__(self, microPythonWidget, deviceType, parent=None): |
|
31 """ |
|
32 Constructor |
|
33 |
|
34 @param microPythonWidget reference to the main MicroPython widget |
|
35 @type MicroPythonWidget |
|
36 @param deviceType device type assigned to this device interface |
|
37 @type str |
|
38 @param parent reference to the parent object |
|
39 @type QObject |
|
40 """ |
|
41 super().__init__(microPythonWidget, deviceType, parent) |
|
42 |
|
43 self.__createEsp32Submenu() |
|
44 |
|
45 def setButtons(self): |
|
46 """ |
|
47 Public method to enable the supported action buttons. |
|
48 """ |
|
49 super().setButtons() |
|
50 self.microPython.setActionButtons( |
|
51 run=True, repl=True, files=True, chart=HAS_QTCHART |
|
52 ) |
|
53 |
|
54 def forceInterrupt(self): |
|
55 """ |
|
56 Public method to determine the need for an interrupt when opening the |
|
57 serial connection. |
|
58 |
|
59 @return flag indicating an interrupt is needed |
|
60 @rtype bool |
|
61 """ |
|
62 return True |
|
63 |
|
64 def deviceName(self): |
|
65 """ |
|
66 Public method to get the name of the device. |
|
67 |
|
68 @return name of the device |
|
69 @rtype str |
|
70 """ |
|
71 return self.tr("ESP8266, ESP32") |
|
72 |
|
73 def canStartRepl(self): |
|
74 """ |
|
75 Public method to determine, if a REPL can be started. |
|
76 |
|
77 @return tuple containing a flag indicating it is safe to start a REPL |
|
78 and a reason why it cannot. |
|
79 @rtype tuple of (bool, str) |
|
80 """ |
|
81 return True, "" |
|
82 |
|
83 def canStartPlotter(self): |
|
84 """ |
|
85 Public method to determine, if a Plotter can be started. |
|
86 |
|
87 @return tuple containing a flag indicating it is safe to start a |
|
88 Plotter and a reason why it cannot. |
|
89 @rtype tuple of (bool, str) |
|
90 """ |
|
91 return True, "" |
|
92 |
|
93 def canRunScript(self): |
|
94 """ |
|
95 Public method to determine, if a script can be executed. |
|
96 |
|
97 @return tuple containing a flag indicating it is safe to start a |
|
98 Plotter and a reason why it cannot. |
|
99 @rtype tuple of (bool, str) |
|
100 """ |
|
101 return True, "" |
|
102 |
|
103 def runScript(self, script): |
|
104 """ |
|
105 Public method to run the given Python script. |
|
106 |
|
107 @param script script to be executed |
|
108 @type str |
|
109 """ |
|
110 pythonScript = script.split("\n") |
|
111 self.sendCommands(pythonScript) |
|
112 |
|
113 def canStartFileManager(self): |
|
114 """ |
|
115 Public method to determine, if a File Manager can be started. |
|
116 |
|
117 @return tuple containing a flag indicating it is safe to start a |
|
118 File Manager and a reason why it cannot. |
|
119 @rtype tuple of (bool, str) |
|
120 """ |
|
121 return True, "" |
|
122 |
|
123 def __createEsp32Submenu(self): |
|
124 """ |
|
125 Private method to create the ESP32 submenu. |
|
126 """ |
|
127 self.__espMenu = QMenu(self.tr("ESP32 Functions")) |
|
128 |
|
129 self.__showMpyAct = self.__espMenu.addAction( |
|
130 self.tr("Show MicroPython Versions"), self.__showFirmwareVersions |
|
131 ) |
|
132 self.__espMenu.addSeparator() |
|
133 self.__eraseFlashAct = self.__espMenu.addAction( |
|
134 self.tr("Erase Flash"), self.__eraseFlash |
|
135 ) |
|
136 self.__flashMpyAct = self.__espMenu.addAction( |
|
137 self.tr("Flash MicroPython Firmware"), self.__flashMicroPython |
|
138 ) |
|
139 self.__espMenu.addSeparator() |
|
140 self.__flashAdditionalAct = self.__espMenu.addAction( |
|
141 self.tr("Flash Additional Firmware"), self.__flashAddons |
|
142 ) |
|
143 self.__espMenu.addSeparator() |
|
144 self.__backupAct = self.__espMenu.addAction( |
|
145 self.tr("Backup Firmware"), self.__backupFlash |
|
146 ) |
|
147 self.__restoreAct = self.__espMenu.addAction( |
|
148 self.tr("Restore Firmware"), self.__restoreFlash |
|
149 ) |
|
150 self.__espMenu.addSeparator() |
|
151 self.__chipIdAct = self.__espMenu.addAction( |
|
152 self.tr("Show Chip ID"), self.__showChipID |
|
153 ) |
|
154 self.__flashIdAct = self.__espMenu.addAction( |
|
155 self.tr("Show Flash ID"), self.__showFlashID |
|
156 ) |
|
157 self.__macAddressAct = self.__espMenu.addAction( |
|
158 self.tr("Show MAC Address"), self.__showMACAddress |
|
159 ) |
|
160 self.__espMenu.addSeparator() |
|
161 self.__resetAct = self.__espMenu.addAction( |
|
162 self.tr("Reset Device"), self.__resetDevice |
|
163 ) |
|
164 self.__espMenu.addSeparator() |
|
165 self.__espMenu.addAction(self.tr("Install 'esptool.py'"), self.__installEspTool) |
|
166 |
|
167 def addDeviceMenuEntries(self, menu): |
|
168 """ |
|
169 Public method to add device specific entries to the given menu. |
|
170 |
|
171 @param menu reference to the context menu |
|
172 @type QMenu |
|
173 """ |
|
174 connected = self.microPython.isConnected() |
|
175 linkConnected = self.microPython.isLinkConnected() |
|
176 |
|
177 self.__showMpyAct.setEnabled(connected) |
|
178 self.__eraseFlashAct.setEnabled(not linkConnected) |
|
179 self.__flashMpyAct.setEnabled(not linkConnected) |
|
180 self.__flashAdditionalAct.setEnabled(not linkConnected) |
|
181 self.__backupAct.setEnabled(not linkConnected) |
|
182 self.__restoreAct.setEnabled(not linkConnected) |
|
183 self.__chipIdAct.setEnabled(not linkConnected) |
|
184 self.__flashIdAct.setEnabled(not linkConnected) |
|
185 self.__macAddressAct.setEnabled(not linkConnected) |
|
186 self.__resetAct.setEnabled(connected or not linkConnected) |
|
187 |
|
188 menu.addMenu(self.__espMenu) |
|
189 |
|
190 def hasFlashMenuEntry(self): |
|
191 """ |
|
192 Public method to check, if the device has its own flash menu entry. |
|
193 |
|
194 @return flag indicating a specific flash menu entry |
|
195 @rtype bool |
|
196 """ |
|
197 return True |
|
198 |
|
199 @pyqtSlot() |
|
200 def __eraseFlash(self): |
|
201 """ |
|
202 Private slot to erase the device flash memory. |
|
203 """ |
|
204 ok = EricMessageBox.yesNo( |
|
205 self.microPython, |
|
206 self.tr("Erase Flash"), |
|
207 self.tr("""Shall the flash of the selected device really be erased?"""), |
|
208 ) |
|
209 if ok: |
|
210 flashArgs = [ |
|
211 "-u", |
|
212 "-m", |
|
213 "esptool", |
|
214 "--port", |
|
215 self.microPython.getCurrentPort(), |
|
216 "erase_flash", |
|
217 ] |
|
218 dlg = EricProcessDialog( |
|
219 self.tr("'esptool erase_flash' Output"), |
|
220 self.tr("Erase Flash"), |
|
221 showProgress=True, |
|
222 ) |
|
223 res = dlg.startProcess(PythonUtilities.getPythonExecutable(), flashArgs) |
|
224 if res: |
|
225 dlg.exec() |
|
226 |
|
227 @pyqtSlot() |
|
228 def __flashMicroPython(self): |
|
229 """ |
|
230 Private slot to flash a MicroPython firmware to the device. |
|
231 """ |
|
232 from .EspFirmwareSelectionDialog import EspFirmwareSelectionDialog |
|
233 |
|
234 dlg = EspFirmwareSelectionDialog() |
|
235 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
236 chip, firmware, baudRate, flashMode, flashAddress = dlg.getData() |
|
237 flashArgs = [ |
|
238 "-u", |
|
239 "-m", |
|
240 "esptool", |
|
241 "--chip", |
|
242 chip, |
|
243 "--port", |
|
244 self.microPython.getCurrentPort(), |
|
245 ] |
|
246 if baudRate != "115200": |
|
247 flashArgs += ["--baud", baudRate] |
|
248 flashArgs.append("write_flash") |
|
249 if flashMode: |
|
250 flashArgs += ["--flash_mode", flashMode] |
|
251 flashArgs += [ |
|
252 flashAddress, |
|
253 firmware, |
|
254 ] |
|
255 dlg = EricProcessDialog( |
|
256 self.tr("'esptool write_flash' Output"), |
|
257 self.tr("Flash MicroPython Firmware"), |
|
258 showProgress=True, |
|
259 ) |
|
260 res = dlg.startProcess(PythonUtilities.getPythonExecutable(), flashArgs) |
|
261 if res: |
|
262 dlg.exec() |
|
263 |
|
264 @pyqtSlot() |
|
265 def __flashAddons(self): |
|
266 """ |
|
267 Private slot to flash some additional firmware images. |
|
268 """ |
|
269 from .EspFirmwareSelectionDialog import EspFirmwareSelectionDialog |
|
270 |
|
271 dlg = EspFirmwareSelectionDialog(addon=True) |
|
272 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
273 chip, firmware, baudRate, flashMode, flashAddress = dlg.getData() |
|
274 flashArgs = [ |
|
275 "-u", |
|
276 "-m", |
|
277 "esptool", |
|
278 "--chip", |
|
279 chip, |
|
280 "--port", |
|
281 self.microPython.getCurrentPort(), |
|
282 ] |
|
283 if baudRate != "115200": |
|
284 flashArgs += ["--baud", baudRate] |
|
285 flashArgs.append("write_flash") |
|
286 if flashMode: |
|
287 flashArgs += ["--flash_mode", flashMode] |
|
288 flashArgs += [ |
|
289 flashAddress.lower(), |
|
290 firmware, |
|
291 ] |
|
292 dlg = EricProcessDialog( |
|
293 self.tr("'esptool write_flash' Output"), |
|
294 self.tr("Flash Additional Firmware"), |
|
295 showProgress=True, |
|
296 ) |
|
297 res = dlg.startProcess(PythonUtilities.getPythonExecutable(), flashArgs) |
|
298 if res: |
|
299 dlg.exec() |
|
300 |
|
301 @pyqtSlot() |
|
302 def __backupFlash(self): |
|
303 """ |
|
304 Private slot to backup the currently flashed firmware. |
|
305 """ |
|
306 from .EspBackupRestoreFirmwareDialog import EspBackupRestoreFirmwareDialog |
|
307 |
|
308 dlg = EspBackupRestoreFirmwareDialog(backupMode=True) |
|
309 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
310 chip, flashSize, baudRate, flashMode, firmware = dlg.getData() |
|
311 flashArgs = [ |
|
312 "-u", |
|
313 "-m", |
|
314 "esptool", |
|
315 "--chip", |
|
316 chip, |
|
317 "--port", |
|
318 self.microPython.getCurrentPort(), |
|
319 "--baud", |
|
320 baudRate, |
|
321 "read_flash", |
|
322 "0x0", |
|
323 flashSize, |
|
324 firmware, |
|
325 ] |
|
326 dlg = EricProcessDialog( |
|
327 self.tr("'esptool read_flash' Output"), |
|
328 self.tr("Backup Firmware"), |
|
329 showProgress=True, |
|
330 ) |
|
331 res = dlg.startProcess(PythonUtilities.getPythonExecutable(), flashArgs) |
|
332 if res: |
|
333 dlg.exec() |
|
334 |
|
335 @pyqtSlot() |
|
336 def __restoreFlash(self): |
|
337 """ |
|
338 Private slot to restore a previously saved firmware. |
|
339 """ |
|
340 from .EspBackupRestoreFirmwareDialog import EspBackupRestoreFirmwareDialog |
|
341 |
|
342 dlg = EspBackupRestoreFirmwareDialog(backupMode=False) |
|
343 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
344 chip, flashSize, baudRate, flashMode, firmware = dlg.getData() |
|
345 flashArgs = [ |
|
346 "-u", |
|
347 "-m", |
|
348 "esptool", |
|
349 "--chip", |
|
350 chip, |
|
351 "--port", |
|
352 self.microPython.getCurrentPort(), |
|
353 "--baud", |
|
354 baudRate, |
|
355 "write_flash", |
|
356 ] |
|
357 if flashMode: |
|
358 flashArgs.extend( |
|
359 [ |
|
360 "--flash_mode", |
|
361 flashMode, |
|
362 ] |
|
363 ) |
|
364 if bool(flashSize): |
|
365 flashArgs.extend( |
|
366 [ |
|
367 "--flash_size", |
|
368 flashSize, |
|
369 ] |
|
370 ) |
|
371 flashArgs.extend( |
|
372 [ |
|
373 "0x0", |
|
374 firmware, |
|
375 ] |
|
376 ) |
|
377 dlg = EricProcessDialog( |
|
378 self.tr("'esptool write_flash' Output"), |
|
379 self.tr("Restore Firmware"), |
|
380 showProgress=True, |
|
381 ) |
|
382 res = dlg.startProcess(PythonUtilities.getPythonExecutable(), flashArgs) |
|
383 if res: |
|
384 dlg.exec() |
|
385 |
|
386 @pyqtSlot() |
|
387 def __showFirmwareVersions(self): |
|
388 """ |
|
389 Private slot to show the firmware version of the connected device and the |
|
390 available firmware version. |
|
391 """ |
|
392 if self.microPython.isConnected(): |
|
393 if self._deviceData["mpy_name"] == "micropython": |
|
394 url = QUrl(FirmwareGithubUrls["micropython"]) |
|
395 elif self._deviceData["mpy_name"] == "circuitpython": |
|
396 url = QUrl(FirmwareGithubUrls["circuitpython"]) |
|
397 else: |
|
398 EricMessageBox.critical( |
|
399 None, |
|
400 self.tr("Show MicroPython Versions"), |
|
401 self.tr( |
|
402 """The firmware of the connected device cannot be""" |
|
403 """ determined or the board does not run MicroPython""" |
|
404 """ or CircuitPython. Aborting...""" |
|
405 ), |
|
406 ) |
|
407 return |
|
408 |
|
409 ui = ericApp().getObject("UserInterface") |
|
410 request = QNetworkRequest(url) |
|
411 reply = ui.networkAccessManager().head(request) |
|
412 reply.finished.connect(lambda: self.__firmwareVersionResponse(reply)) |
|
413 |
|
414 def __firmwareVersionResponse(self, reply): |
|
415 """ |
|
416 Private method handling the response of the latest version request. |
|
417 |
|
418 @param reply reference to the reply object |
|
419 @type QNetworkReply |
|
420 """ |
|
421 latestUrl = reply.url().toString() |
|
422 tag = latestUrl.rsplit("/", 1)[-1] |
|
423 while tag and not tag[0].isdecimal(): |
|
424 # get rid of leading non-decimal characters |
|
425 tag = tag[1:] |
|
426 latestVersion = Globals.versionToTuple(tag) |
|
427 |
|
428 if self._deviceData["mpy_version"] == "unknown": |
|
429 currentVersionStr = self.tr("unknown") |
|
430 currentVersion = (0, 0, 0) |
|
431 else: |
|
432 currentVersionStr = self._deviceData["mpy_version"] |
|
433 currentVersion = Globals.versionToTuple(currentVersionStr) |
|
434 |
|
435 if self._deviceData["mpy_name"] == "circuitpython": |
|
436 kind = "CircuitPython" |
|
437 elif self._deviceData["mpy_name"] == "micropython": |
|
438 kind = "MicroPython" |
|
439 |
|
440 msg = self.tr( |
|
441 "<h4>{0} Version Information</h4>" |
|
442 "<table>" |
|
443 "<tr><td>Installed:</td><td>{1}</td></tr>" |
|
444 "<tr><td>Available:</td><td>{2}</td></tr>" |
|
445 "</table>" |
|
446 ).format(kind, currentVersionStr, tag) |
|
447 if currentVersion < latestVersion: |
|
448 msg += self.tr("<p><b>Update available!</b></p>") |
|
449 |
|
450 EricMessageBox.information( |
|
451 None, |
|
452 self.tr("{0} Version").format(kind), |
|
453 msg, |
|
454 ) |
|
455 |
|
456 @pyqtSlot() |
|
457 def __showChipID(self): |
|
458 """ |
|
459 Private slot to show the ID of the ESP chip. |
|
460 """ |
|
461 args = [ |
|
462 "-u", |
|
463 "-m", |
|
464 "esptool", |
|
465 "--port", |
|
466 self.microPython.getCurrentPort(), |
|
467 "chip_id", |
|
468 ] |
|
469 dlg = EricProcessDialog( |
|
470 self.tr("'esptool chip_id' Output"), self.tr("Show Chip ID") |
|
471 ) |
|
472 res = dlg.startProcess(PythonUtilities.getPythonExecutable(), args) |
|
473 if res: |
|
474 dlg.exec() |
|
475 |
|
476 @pyqtSlot() |
|
477 def __showFlashID(self): |
|
478 """ |
|
479 Private slot to show the ID of the ESP flash chip. |
|
480 """ |
|
481 args = [ |
|
482 "-u", |
|
483 "-m", |
|
484 "esptool", |
|
485 "--port", |
|
486 self.microPython.getCurrentPort(), |
|
487 "flash_id", |
|
488 ] |
|
489 dlg = EricProcessDialog( |
|
490 self.tr("'esptool flash_id' Output"), self.tr("Show Flash ID") |
|
491 ) |
|
492 res = dlg.startProcess(PythonUtilities.getPythonExecutable(), args) |
|
493 if res: |
|
494 dlg.exec() |
|
495 |
|
496 @pyqtSlot() |
|
497 def __showMACAddress(self): |
|
498 """ |
|
499 Private slot to show the MAC address of the ESP chip. |
|
500 """ |
|
501 args = [ |
|
502 "-u", |
|
503 "-m", |
|
504 "esptool", |
|
505 "--port", |
|
506 self.microPython.getCurrentPort(), |
|
507 "read_mac", |
|
508 ] |
|
509 dlg = EricProcessDialog( |
|
510 self.tr("'esptool read_mac' Output"), self.tr("Show MAC Address") |
|
511 ) |
|
512 res = dlg.startProcess(PythonUtilities.getPythonExecutable(), args) |
|
513 if res: |
|
514 dlg.exec() |
|
515 |
|
516 @pyqtSlot() |
|
517 def __resetDevice(self): |
|
518 """ |
|
519 Private slot to reset the connected device. |
|
520 """ |
|
521 if self.microPython.isConnected(): |
|
522 self.microPython.commandsInterface().execute( |
|
523 [ |
|
524 "import machine", |
|
525 "machine.reset()", |
|
526 ] |
|
527 ) |
|
528 else: |
|
529 # perform a reset via esptool using flash_id command ignoring |
|
530 # the output |
|
531 args = [ |
|
532 "-u", |
|
533 "-m", |
|
534 "esptool", |
|
535 "--port", |
|
536 self.microPython.getCurrentPort(), |
|
537 "flash_id", |
|
538 ] |
|
539 proc = QProcess() |
|
540 proc.start(PythonUtilities.getPythonExecutable(), args) |
|
541 procStarted = proc.waitForStarted(10000) |
|
542 if procStarted: |
|
543 proc.waitForFinished(10000) |
|
544 |
|
545 @pyqtSlot() |
|
546 def __installEspTool(self): |
|
547 """ |
|
548 Private slot to install the esptool package via pip. |
|
549 """ |
|
550 pip = ericApp().getObject("Pip") |
|
551 pip.installPackages( |
|
552 ["esptool"], interpreter=PythonUtilities.getPythonExecutable() |
|
553 ) |
|
554 |
|
555 def getDocumentationUrl(self): |
|
556 """ |
|
557 Public method to get the device documentation URL. |
|
558 |
|
559 @return documentation URL of the device |
|
560 @rtype str |
|
561 """ |
|
562 return Preferences.getMicroPython("MicroPythonDocuUrl") |
|
563 |
|
564 def getFirmwareUrl(self): |
|
565 """ |
|
566 Public method to get the device firmware download URL. |
|
567 |
|
568 @return firmware download URL of the device |
|
569 @rtype str |
|
570 """ |
|
571 return Preferences.getMicroPython("MicroPythonFirmwareUrl") |
|
572 |
|
573 |
|
574 def createDevice(microPythonWidget, deviceType, vid, pid, boardName, serialNumber): |
|
575 """ |
|
576 Function to instantiate a MicroPython device object. |
|
577 |
|
578 @param microPythonWidget reference to the main MicroPython widget |
|
579 @type MicroPythonWidget |
|
580 @param deviceType device type assigned to this device interface |
|
581 @type str |
|
582 @param vid vendor ID |
|
583 @type int |
|
584 @param pid product ID |
|
585 @type int |
|
586 @param boardName name of the board |
|
587 @type str |
|
588 @param serialNumber serial number of the board |
|
589 @type str |
|
590 @return reference to the instantiated device object |
|
591 @rtype EspDevice |
|
592 """ |
|
593 return EspDevice(microPythonWidget, deviceType) |
|