diff -r 1a09700229e7 -r 9854647c8c5c src/eric7/MicroPython/CircuitPythonDevices.py --- a/src/eric7/MicroPython/CircuitPythonDevices.py Sun Feb 12 18:11:20 2023 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,533 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (c) 2019 - 2023 Detlev Offenbach <detlev@die-offenbachs.de> -# - -""" -Module implementing the device interface class for CircuitPython boards. -""" - -import os -import shutil - -from PyQt6.QtCore import QProcess, QUrl, pyqtSlot -from PyQt6.QtNetwork import QNetworkRequest -from PyQt6.QtWidgets import QMenu - -from eric7 import Globals, Preferences -from eric7.EricWidgets import EricFileDialog, EricMessageBox -from eric7.EricWidgets.EricApplication import ericApp -from eric7.SystemUtilities import FileSystemUtilities - -from .CircuitPythonUpdater.CircuitPythonUpdaterInterface import ( - CircuitPythonUpdaterInterface, - isCircupAvailable, -) -from .MicroPythonDevices import FirmwareGithubUrls, MicroPythonDevice -from .MicroPythonWidget import HAS_QTCHART - - -class CircuitPythonDevice(MicroPythonDevice): - """ - Class implementing the device for CircuitPython boards. - """ - - DeviceVolumeName = "CIRCUITPY" - - def __init__(self, microPythonWidget, deviceType, boardName, 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 boardName name of the board - @type str - @param parent reference to the parent object - @type QObject - """ - super().__init__(microPythonWidget, deviceType, parent) - - self.__boardName = boardName - self.__workspace = self.__findWorkspace() - - self.__updater = CircuitPythonUpdaterInterface(self) - - self.__createCPyMenu() - - 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 - ) - - if self.__deviceVolumeMounted(): - self.microPython.setActionButtons(open=True, save=True) - - 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 False - - def deviceName(self): - """ - Public method to get the name of the device. - - @return name of the device - @rtype str - """ - return self.tr("CircuitPython") - - 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 supportsLocalFileAccess(self): - """ - Public method to indicate file access via a local directory. - - @return flag indicating file access via local directory - @rtype bool - """ - return self.__deviceVolumeMounted() - - def __deviceVolumeMounted(self): - """ - Private method to check, if the device volume is mounted. - - @return flag indicated a mounted device - @rtype bool - """ - if self.__workspace and not os.path.exists(self.__workspace): - self.__workspace = "" # reset - - return self.DeviceVolumeName in self.getWorkspace(silent=True) - - def __findDeviceDirectories(self, directories): - """ - Private method to find the device directories associated with the - current board name. - - @param directories list of directories to be checked - @type list of str - @return list of associated directories - @rtype list of str - """ - boardDirectories = [] - for directory in directories: - bootFile = os.path.join(directory, "boot_out.txt") - if os.path.exists(bootFile): - with open(bootFile, "r") as f: - line = f.readline() - if self.__boardName in line: - boardDirectories.append(directory) - - return boardDirectories - - def __findWorkspace(self, silent=False): - """ - Private method to find the workspace directory. - - @param silent flag indicating silent operations - @type bool - @return workspace directory used for saving files - @rtype str - """ - # Attempts to find the paths on the filesystem that represents the - # plugged in CIRCUITPY boards. - deviceDirectories = FileSystemUtilities.findVolume( - self.DeviceVolumeName, findAll=True - ) - - if deviceDirectories: - if len(deviceDirectories) == 1: - return deviceDirectories[0] - else: - boardDirectories = self.__findDeviceDirectories(deviceDirectories) - if len(boardDirectories) == 1: - return boardDirectories[0] - elif len(boardDirectories) > 1: - return self.selectDeviceDirectory(boardDirectories) - else: - return self.selectDeviceDirectory(deviceDirectories) - else: - # return the default workspace and give the user a warning (unless - # silent mode is selected) - if not silent: - EricMessageBox.warning( - self.microPython, - self.tr("Workspace Directory"), - self.tr( - "Python files for CircuitPython can be edited in" - " place, if the device volume is locally" - " available. Such a volume was not found. In" - " place editing will not be available." - ), - ) - - return super().getWorkspace() - - def getWorkspace(self, silent=False): - """ - Public method to get the workspace directory. - - @param silent flag indicating silent operations - @type bool - @return workspace directory used for saving files - @rtype str - """ - if self.__workspace: - # return cached entry - return self.__workspace - else: - self.__workspace = self.__findWorkspace(silent=silent) - return self.__workspace - - def __createCPyMenu(self): - """ - Private method to create the CircuitPython submenu. - """ - self.__libraryMenu = QMenu(self.tr("Library Management")) - self.__libraryMenu.aboutToShow.connect(self.__aboutToShowLibraryMenu) - self.__libraryMenu.setTearOffEnabled(True) - - self.__cpyMenu = QMenu(self.tr("CircuitPython Functions")) - - self.__cpyMenu.addAction( - self.tr("Show CircuitPython Versions"), self.__showCircuitPythonVersions - ) - self.__cpyMenu.addSeparator() - - lBoardName = self.microPython.getCurrentBoard().lower() - if "teensy" in lBoardName: - # Teensy 4.0 and 4.1 don't support UF2 flashing - self.__cpyMenu.addAction( - self.tr("CircuitPython Flash Instructions"), - self.__showTeensyFlashInstructions, - ) - self.__flashCpyAct = self.__cpyMenu.addAction( - self.tr("Flash CircuitPython Firmware"), self.__startTeensyLoader - ) - self.__flashCpyAct.setToolTip( - self.tr( - "Start the 'Teensy Loader' application to flash the Teensy device." - ) - ) - else: - self.__flashCpyAct = self.__cpyMenu.addAction( - self.tr("Flash CircuitPython Firmware"), self.__flashCircuitPython - ) - self.__cpyMenu.addSeparator() - self.__cpyMenu.addMenu(self.__libraryMenu) - - def addDeviceMenuEntries(self, menu): - """ - Public method to add device specific entries to the given menu. - - @param menu reference to the context menu - @type QMenu - """ - linkConnected = self.microPython.isLinkConnected() - - self.__flashCpyAct.setEnabled(not linkConnected) - - menu.addMenu(self.__cpyMenu) - - @pyqtSlot() - def __aboutToShowLibraryMenu(self): - """ - Private slot to populate the 'Library Management' menu. - """ - self.__libraryMenu.clear() - - if isCircupAvailable(): - self.__updater.populateMenu(self.__libraryMenu) - else: - act = self.__libraryMenu.addAction( - self.tr("Install Library Files"), self.__installLibraryFiles - ) - act.setEnabled(self.__deviceVolumeMounted()) - act = self.__libraryMenu.addAction( - self.tr("Install Library Package"), - lambda: self.__installLibraryFiles(packageMode=True), - ) - act.setEnabled(self.__deviceVolumeMounted()) - self.__libraryMenu.addSeparator() - self.__libraryMenu.addAction( - self.tr("Install 'circup' Package"), - self.__updater.installCircup, - ) - - 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 __flashCircuitPython(self): - """ - Private slot to flash a CircuitPython firmware to a device supporting UF2. - """ - from .UF2FlashDialog import UF2FlashDialog - - dlg = UF2FlashDialog(boardType="circuitpython") - dlg.exec() - - def __showTeensyFlashInstructions(self): - """ - Private method to show a message box because Teensy does not support - the UF2 bootloader yet. - """ - EricMessageBox.information( - self.microPython, - self.tr("Flash CircuitPython Firmware"), - self.tr( - """<p>Teensy 4.0 and Teensy 4.1 do not support the UF2""" - """ bootloader. Please use the 'Teensy Loader'""" - """ application to flash CircuitPython. Make sure you""" - """ downloaded the CircuitPython .hex file.</p>""" - """<p>See <a href="{0}">the PJRC Teensy web site</a>""" - """ for details.</p>""" - ).format("https://www.pjrc.com/teensy/loader.html"), - ) - - def __startTeensyLoader(self): - """ - Private method to start the 'Teensy Loader' application. - - Note: The application must be accessible via the application search path. - """ - ok, _ = QProcess.startDetached("teensy") - if not ok: - EricMessageBox.warning( - self.microPython, - self.tr("Start 'Teensy Loader'"), - self.tr( - """<p>The 'Teensy Loader' application <b>teensy</b> could not""" - """ be started. Ensure it is in the application search path or""" - """ start it manually.</p>""" - ), - ) - - @pyqtSlot() - def __showCircuitPythonVersions(self): - """ - Private slot to show the CircuitPython version of a connected device and - the latest available one (from Github). - """ - ui = ericApp().getObject("UserInterface") - request = QNetworkRequest(QUrl(FirmwareGithubUrls["circuitpython"])) - reply = ui.networkAccessManager().head(request) - reply.finished.connect(lambda: self.__cpyVersionResponse(reply)) - - def __cpyVersionResponse(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] - latestVersion = Globals.versionToTuple(tag) - - cpyVersionStr = self.tr("unknown") - cpyVersion = (0, 0, 0) - if self.supportsLocalFileAccess(): - bootFile = os.path.join(self.getWorkspace(), "boot_out.txt") - if os.path.exists(bootFile): - with open(bootFile, "r") as f: - line = f.readline() - cpyVersionStr = line.split(";")[0].split()[2] - cpyVersion = Globals.versionToTuple(cpyVersionStr) - if ( - cpyVersion == (0, 0, 0) - and self._deviceData - and self._deviceData["mpy_version"] != "unknown" - ): - # drive is not mounted or 'boot_out.txt' is missing but the device - # is connected via the serial console - cpyVersionStr = self._deviceData["mpy_version"] - cpyVersion = Globals.versionToTuple(cpyVersionStr) - - msg = self.tr( - "<h4>CircuitPython Version Information</h4>" - "<table>" - "<tr><td>Installed:</td><td>{0}</td></tr>" - "<tr><td>Available:</td><td>{1}</td></tr>" - "</table>" - ).format(cpyVersionStr, tag) - if cpyVersion < latestVersion and cpyVersion != (0, 0, 0): - msg += self.tr("<p><b>Update available!</b></p>") - - EricMessageBox.information( - None, - self.tr("CircuitPython Version"), - msg, - ) - - @pyqtSlot() - def __installLibraryFiles(self, packageMode=False): - """ - Private slot to install Python files into the onboard library. - - @param packageMode flag indicating to install a library package - (defaults to False) - @type bool (optional) - """ - title = ( - self.tr("Install Library Package") - if packageMode - else self.tr("Install Library Files") - ) - if not self.__deviceVolumeMounted(): - EricMessageBox.critical( - self.microPython, - title, - self.tr( - """The device volume "<b>{0}</b>" is not available.""" - """ Ensure it is mounted properly and try again.""" - ), - ) - return - - target = os.path.join(self.getWorkspace(), "lib") - # ensure that the library directory exists on the device - if not os.path.isdir(target): - os.makedirs(target) - - if packageMode: - libraryPackage = EricFileDialog.getExistingDirectory( - self.microPython, - title, - os.path.expanduser("~"), - EricFileDialog.Option(0), - ) - if libraryPackage: - target = os.path.join(target, os.path.basename(libraryPackage)) - shutil.rmtree(target, ignore_errors=True) - shutil.copytree(libraryPackage, target) - else: - libraryFiles = EricFileDialog.getOpenFileNames( - self.microPython, - title, - os.path.expanduser("~"), - self.tr( - "Compiled Python Files (*.mpy);;" - "Python Files (*.py);;" - "All Files (*)" - ), - ) - - for libraryFile in libraryFiles: - if os.path.exists(libraryFile): - shutil.copy2(libraryFile, target) - - def getDocumentationUrl(self): - """ - Public method to get the device documentation URL. - - @return documentation URL of the device - @rtype str - """ - return Preferences.getMicroPython("CircuitPythonDocuUrl") - - def getDownloadMenuEntries(self): - """ - Public method to retrieve the entries for the downloads menu. - - @return list of tuples with menu text and URL to be opened for each - entry - @rtype list of tuple of (str, str) - """ - return [ - ( - self.tr("CircuitPython Firmware"), - Preferences.getMicroPython("CircuitPythonFirmwareUrl"), - ), - ( - self.tr("CircuitPython Libraries"), - Preferences.getMicroPython("CircuitPythonLibrariesUrl"), - ), - ] - - -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 CircuitPythonDevice - """ - return CircuitPythonDevice(microPythonWidget, deviceType, boardName)