src/eric7/MicroPython/Devices/EspDevices.py

branch
eric7
changeset 9756
9854647c8c5c
parent 9752
2b9546c0cbd9
child 9763
52f982c08301
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)

eric ide

mercurial