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