diff -r 1a09700229e7 -r 9854647c8c5c src/eric7/MicroPython/Devices/EspDevices.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/MicroPython/Devices/EspDevices.py Mon Feb 13 17:49:52 2023 +0100 @@ -0,0 +1,598 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2019 - 2023 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the device interface class for ESP32 and ESP8266 based +boards. +""" + +from PyQt6.QtCore import QProcess, QUrl, pyqtSlot +from PyQt6.QtNetwork import QNetworkRequest +from PyQt6.QtWidgets import QDialog, QMenu + +from eric7 import Globals, Preferences +from eric7.EricWidgets import EricMessageBox +from eric7.EricWidgets.EricApplication import ericApp +from eric7.EricWidgets.EricProcessDialog import EricProcessDialog +from eric7.SystemUtilities import PythonUtilities + +from . import FirmwareGithubUrls +from .DeviceBase import BaseDevice +from ..MicroPythonWidget import HAS_QTCHART + + +class EspDevice(BaseDevice): + """ + Class implementing the device for ESP32 and ESP8266 based boards. + """ + + def __init__(self, microPythonWidget, deviceType, parent=None): + """ + Constructor + + @param microPythonWidget reference to the main MicroPython widget + @type MicroPythonWidget + @param deviceType device type assigned to this device interface + @type str + @param parent reference to the parent object + @type QObject + """ + super().__init__(microPythonWidget, deviceType, parent) + + self.__createEsp32Submenu() + + def setButtons(self): + """ + Public method to enable the supported action buttons. + """ + super().setButtons() + self.microPython.setActionButtons( + run=True, repl=True, files=True, chart=HAS_QTCHART + ) + + def forceInterrupt(self): + """ + Public method to determine the need for an interrupt when opening the + serial connection. + + @return flag indicating an interrupt is needed + @rtype bool + """ + return True + + def deviceName(self): + """ + Public method to get the name of the device. + + @return name of the device + @rtype str + """ + return self.tr("ESP8266, ESP32") + + def canStartRepl(self): + """ + Public method to determine, if a REPL can be started. + + @return tuple containing a flag indicating it is safe to start a REPL + and a reason why it cannot. + @rtype tuple of (bool, str) + """ + return True, "" + + def canStartPlotter(self): + """ + Public method to determine, if a Plotter can be started. + + @return tuple containing a flag indicating it is safe to start a + Plotter and a reason why it cannot. + @rtype tuple of (bool, str) + """ + return True, "" + + def canRunScript(self): + """ + Public method to determine, if a script can be executed. + + @return tuple containing a flag indicating it is safe to start a + Plotter and a reason why it cannot. + @rtype tuple of (bool, str) + """ + return True, "" + + def runScript(self, script): + """ + Public method to run the given Python script. + + @param script script to be executed + @type str + """ + pythonScript = script.split("\n") + self.sendCommands(pythonScript) + + def canStartFileManager(self): + """ + Public method to determine, if a File Manager can be started. + + @return tuple containing a flag indicating it is safe to start a + File Manager and a reason why it cannot. + @rtype tuple of (bool, str) + """ + return True, "" + + def __createEsp32Submenu(self): + """ + Private method to create the ESP32 submenu. + """ + self.__espMenu = QMenu(self.tr("ESP32 Functions")) + + self.__showMpyAct = self.__espMenu.addAction( + self.tr("Show MicroPython Versions"), self.__showFirmwareVersions + ) + self.__espMenu.addSeparator() + self.__eraseFlashAct = self.__espMenu.addAction( + self.tr("Erase Flash"), self.__eraseFlash + ) + self.__flashMpyAct = self.__espMenu.addAction( + self.tr("Flash MicroPython Firmware"), self.__flashMicroPython + ) + self.__espMenu.addSeparator() + self.__flashAdditionalAct = self.__espMenu.addAction( + self.tr("Flash Additional Firmware"), self.__flashAddons + ) + self.__espMenu.addSeparator() + self.__backupAct = self.__espMenu.addAction( + self.tr("Backup Firmware"), self.__backupFlash + ) + self.__restoreAct = self.__espMenu.addAction( + self.tr("Restore Firmware"), self.__restoreFlash + ) + self.__espMenu.addSeparator() + self.__chipIdAct = self.__espMenu.addAction( + self.tr("Show Chip ID"), self.__showChipID + ) + self.__flashIdAct = self.__espMenu.addAction( + self.tr("Show Flash ID"), self.__showFlashID + ) + self.__macAddressAct = self.__espMenu.addAction( + self.tr("Show MAC Address"), self.__showMACAddress + ) + self.__espMenu.addSeparator() + self.__resetAct = self.__espMenu.addAction( + self.tr("Reset Device"), self.__resetDevice + ) + self.__espMenu.addSeparator() + self.__espMenu.addAction(self.tr("Install 'esptool.py'"), self.__installEspTool) + + def addDeviceMenuEntries(self, menu): + """ + Public method to add device specific entries to the given menu. + + @param menu reference to the context menu + @type QMenu + """ + connected = self.microPython.isConnected() + linkConnected = self.microPython.isLinkConnected() + + self.__showMpyAct.setEnabled(connected) + self.__eraseFlashAct.setEnabled(not linkConnected) + self.__flashMpyAct.setEnabled(not linkConnected) + self.__flashAdditionalAct.setEnabled(not linkConnected) + self.__backupAct.setEnabled(not linkConnected) + self.__restoreAct.setEnabled(not linkConnected) + self.__chipIdAct.setEnabled(not linkConnected) + self.__flashIdAct.setEnabled(not linkConnected) + self.__macAddressAct.setEnabled(not linkConnected) + self.__resetAct.setEnabled(connected or not linkConnected) + + menu.addMenu(self.__espMenu) + + def hasFlashMenuEntry(self): + """ + Public method to check, if the device has its own flash menu entry. + + @return flag indicating a specific flash menu entry + @rtype bool + """ + return True + + @pyqtSlot() + def __eraseFlash(self): + """ + Private slot to erase the device flash memory. + """ + ok = EricMessageBox.yesNo( + self.microPython, + self.tr("Erase Flash"), + self.tr("""Shall the flash of the selected device really be erased?"""), + ) + if ok: + flashArgs = [ + "-u", + "-m", + "esptool", + "--port", + self.microPython.getCurrentPort(), + "erase_flash", + ] + dlg = EricProcessDialog( + self.tr("'esptool erase_flash' Output"), + self.tr("Erase Flash"), + showProgress=True, + ) + res = dlg.startProcess(PythonUtilities.getPythonExecutable(), flashArgs) + if res: + dlg.exec() + + @pyqtSlot() + def __flashMicroPython(self): + """ + Private slot to flash a MicroPython firmware to the device. + """ + from .EspDialogs.EspFirmwareSelectionDialog import EspFirmwareSelectionDialog + + dlg = EspFirmwareSelectionDialog() + if dlg.exec() == QDialog.DialogCode.Accepted: + chip, firmware, baudRate, flashMode, flashAddress = dlg.getData() + flashArgs = [ + "-u", + "-m", + "esptool", + "--chip", + chip, + "--port", + self.microPython.getCurrentPort(), + ] + if baudRate != "115200": + flashArgs += ["--baud", baudRate] + flashArgs.append("write_flash") + if flashMode: + flashArgs += ["--flash_mode", flashMode] + flashArgs += [ + flashAddress, + firmware, + ] + dlg = EricProcessDialog( + self.tr("'esptool write_flash' Output"), + self.tr("Flash MicroPython Firmware"), + showProgress=True, + ) + res = dlg.startProcess(PythonUtilities.getPythonExecutable(), flashArgs) + if res: + dlg.exec() + + @pyqtSlot() + def __flashAddons(self): + """ + Private slot to flash some additional firmware images. + """ + from .EspDialogs.EspFirmwareSelectionDialog import EspFirmwareSelectionDialog + + dlg = EspFirmwareSelectionDialog(addon=True) + if dlg.exec() == QDialog.DialogCode.Accepted: + chip, firmware, baudRate, flashMode, flashAddress = dlg.getData() + flashArgs = [ + "-u", + "-m", + "esptool", + "--chip", + chip, + "--port", + self.microPython.getCurrentPort(), + ] + if baudRate != "115200": + flashArgs += ["--baud", baudRate] + flashArgs.append("write_flash") + if flashMode: + flashArgs += ["--flash_mode", flashMode] + flashArgs += [ + flashAddress.lower(), + firmware, + ] + dlg = EricProcessDialog( + self.tr("'esptool write_flash' Output"), + self.tr("Flash Additional Firmware"), + showProgress=True, + ) + res = dlg.startProcess(PythonUtilities.getPythonExecutable(), flashArgs) + if res: + dlg.exec() + + @pyqtSlot() + def __backupFlash(self): + """ + Private slot to backup the currently flashed firmware. + """ + from .EspDialogs.EspBackupRestoreFirmwareDialog import ( + EspBackupRestoreFirmwareDialog + ) + + dlg = EspBackupRestoreFirmwareDialog(backupMode=True) + if dlg.exec() == QDialog.DialogCode.Accepted: + chip, flashSize, baudRate, flashMode, firmware = dlg.getData() + flashArgs = [ + "-u", + "-m", + "esptool", + "--chip", + chip, + "--port", + self.microPython.getCurrentPort(), + "--baud", + baudRate, + "read_flash", + "0x0", + flashSize, + firmware, + ] + dlg = EricProcessDialog( + self.tr("'esptool read_flash' Output"), + self.tr("Backup Firmware"), + showProgress=True, + ) + res = dlg.startProcess(PythonUtilities.getPythonExecutable(), flashArgs) + if res: + dlg.exec() + + @pyqtSlot() + def __restoreFlash(self): + """ + Private slot to restore a previously saved firmware. + """ + from .EspDialogs.EspBackupRestoreFirmwareDialog import ( + EspBackupRestoreFirmwareDialog + ) + + dlg = EspBackupRestoreFirmwareDialog(backupMode=False) + if dlg.exec() == QDialog.DialogCode.Accepted: + chip, flashSize, baudRate, flashMode, firmware = dlg.getData() + flashArgs = [ + "-u", + "-m", + "esptool", + "--chip", + chip, + "--port", + self.microPython.getCurrentPort(), + "--baud", + baudRate, + "write_flash", + ] + if flashMode: + flashArgs.extend( + [ + "--flash_mode", + flashMode, + ] + ) + if bool(flashSize): + flashArgs.extend( + [ + "--flash_size", + flashSize, + ] + ) + flashArgs.extend( + [ + "0x0", + firmware, + ] + ) + dlg = EricProcessDialog( + self.tr("'esptool write_flash' Output"), + self.tr("Restore Firmware"), + showProgress=True, + ) + res = dlg.startProcess(PythonUtilities.getPythonExecutable(), flashArgs) + if res: + dlg.exec() + + @pyqtSlot() + def __showFirmwareVersions(self): + """ + Private slot to show the firmware version of the connected device and the + available firmware version. + """ + if self.microPython.isConnected(): + if self._deviceData["mpy_name"] == "micropython": + url = QUrl(FirmwareGithubUrls["micropython"]) + elif self._deviceData["mpy_name"] == "circuitpython": + url = QUrl(FirmwareGithubUrls["circuitpython"]) + else: + EricMessageBox.critical( + None, + self.tr("Show MicroPython Versions"), + self.tr( + """The firmware of the connected device cannot be""" + """ determined or the board does not run MicroPython""" + """ or CircuitPython. Aborting...""" + ), + ) + return + + ui = ericApp().getObject("UserInterface") + request = QNetworkRequest(url) + reply = ui.networkAccessManager().head(request) + reply.finished.connect(lambda: self.__firmwareVersionResponse(reply)) + + def __firmwareVersionResponse(self, reply): + """ + Private method handling the response of the latest version request. + + @param reply reference to the reply object + @type QNetworkReply + """ + latestUrl = reply.url().toString() + tag = latestUrl.rsplit("/", 1)[-1] + while tag and not tag[0].isdecimal(): + # get rid of leading non-decimal characters + tag = tag[1:] + latestVersion = Globals.versionToTuple(tag) + + if self._deviceData["mpy_version"] == "unknown": + currentVersionStr = self.tr("unknown") + currentVersion = (0, 0, 0) + else: + currentVersionStr = self._deviceData["mpy_version"] + currentVersion = Globals.versionToTuple(currentVersionStr) + + if self._deviceData["mpy_name"] == "circuitpython": + kind = "CircuitPython" + elif self._deviceData["mpy_name"] == "micropython": + kind = "MicroPython" + + msg = self.tr( + "<h4>{0} Version Information</h4>" + "<table>" + "<tr><td>Installed:</td><td>{1}</td></tr>" + "<tr><td>Available:</td><td>{2}</td></tr>" + "</table>" + ).format(kind, currentVersionStr, tag) + if currentVersion < latestVersion: + msg += self.tr("<p><b>Update available!</b></p>") + + EricMessageBox.information( + None, + self.tr("{0} Version").format(kind), + msg, + ) + + @pyqtSlot() + def __showChipID(self): + """ + Private slot to show the ID of the ESP chip. + """ + args = [ + "-u", + "-m", + "esptool", + "--port", + self.microPython.getCurrentPort(), + "chip_id", + ] + dlg = EricProcessDialog( + self.tr("'esptool chip_id' Output"), self.tr("Show Chip ID") + ) + res = dlg.startProcess(PythonUtilities.getPythonExecutable(), args) + if res: + dlg.exec() + + @pyqtSlot() + def __showFlashID(self): + """ + Private slot to show the ID of the ESP flash chip. + """ + args = [ + "-u", + "-m", + "esptool", + "--port", + self.microPython.getCurrentPort(), + "flash_id", + ] + dlg = EricProcessDialog( + self.tr("'esptool flash_id' Output"), self.tr("Show Flash ID") + ) + res = dlg.startProcess(PythonUtilities.getPythonExecutable(), args) + if res: + dlg.exec() + + @pyqtSlot() + def __showMACAddress(self): + """ + Private slot to show the MAC address of the ESP chip. + """ + args = [ + "-u", + "-m", + "esptool", + "--port", + self.microPython.getCurrentPort(), + "read_mac", + ] + dlg = EricProcessDialog( + self.tr("'esptool read_mac' Output"), self.tr("Show MAC Address") + ) + res = dlg.startProcess(PythonUtilities.getPythonExecutable(), args) + if res: + dlg.exec() + + @pyqtSlot() + def __resetDevice(self): + """ + Private slot to reset the connected device. + """ + if self.microPython.isConnected(): + self.microPython.commandsInterface().execute( + [ + "import machine", + "machine.reset()", + ] + ) + else: + # perform a reset via esptool using flash_id command ignoring + # the output + args = [ + "-u", + "-m", + "esptool", + "--port", + self.microPython.getCurrentPort(), + "flash_id", + ] + proc = QProcess() + proc.start(PythonUtilities.getPythonExecutable(), args) + procStarted = proc.waitForStarted(10000) + if procStarted: + proc.waitForFinished(10000) + + @pyqtSlot() + def __installEspTool(self): + """ + Private slot to install the esptool package via pip. + """ + pip = ericApp().getObject("Pip") + pip.installPackages( + ["esptool"], interpreter=PythonUtilities.getPythonExecutable() + ) + + def getDocumentationUrl(self): + """ + Public method to get the device documentation URL. + + @return documentation URL of the device + @rtype str + """ + return Preferences.getMicroPython("MicroPythonDocuUrl") + + def getFirmwareUrl(self): + """ + Public method to get the device firmware download URL. + + @return firmware download URL of the device + @rtype str + """ + return Preferences.getMicroPython("MicroPythonFirmwareUrl") + + +def createDevice(microPythonWidget, deviceType, vid, pid, boardName, serialNumber): + """ + Function to instantiate a MicroPython device object. + + @param microPythonWidget reference to the main MicroPython widget + @type MicroPythonWidget + @param deviceType device type assigned to this device interface + @type str + @param vid vendor ID + @type int + @param pid product ID + @type int + @param boardName name of the board + @type str + @param serialNumber serial number of the board + @type str + @return reference to the instantiated device object + @rtype EspDevice + """ + return EspDevice(microPythonWidget, deviceType)