diff -r 0457d754fc9a -r a78b83d1062a src/eric7/MicroPython/Devices/STLinkDevices.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/MicroPython/Devices/STLinkDevices.py Sun Apr 02 15:55:20 2023 +0200 @@ -0,0 +1,497 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2023 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the device interface class for STM32 STLink boards. +""" + +import os + +from PyQt6.QtCore import QStandardPaths, QUrl, pyqtSlot +from PyQt6.QtNetwork import QNetworkReply, 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.EricWidgets.EricProcessDialog import EricProcessDialog +from eric7.SystemUtilities import FileSystemUtilities + +from ..MicroPythonWidget import HAS_QTCHART +from . import FirmwareGithubUrls +from .DeviceBase import BaseDevice + + +class STLinkDevice(BaseDevice): + """ + Class implementing the device for PyBoard boards. + """ + + DeviceVolumeName = "NODE_" + + 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._submitMode = "paste" # use 'paste' mode + + self.__workspace = self.__findWorkspace() + + self.__createSTLinkMenu() + + 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 False + + def deviceName(self): + """ + Public method to get the name of the device. + + @return name of the device + @rtype str + """ + return self.tr("STM32 STLink") + + 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 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 __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 path on the filesystem that represents the + # plugged in PyBoard board. + deviceDirectories = FileSystemUtilities.findVolume( + self.DeviceVolumeName, findAll=True + ) + + if deviceDirectories: + if len(deviceDirectories) == 1: + return deviceDirectories[0] + 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 STLink boards 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 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 __createSTLinkMenu(self): + """ + Private method to create the STLink submenu. + """ + self.__stlinkMenu = QMenu(self.tr("STLink Functions")) + + self.__showMpyAct = self.__stlinkMenu.addAction( + self.tr("Show MicroPython Versions"), self.__showFirmwareVersions + ) + self.__stlinkMenu.addSeparator() + self.__stlinkInfoAct = self.__stlinkMenu.addAction( + self.tr("Show STLink Device Information"), self.__showDeviceInfo + ) + self.__stlinkMenu.addSeparator() + self.__flashMpyAct = self.__stlinkMenu.addAction( + self.tr("Flash MicroPython Firmware"), self.__flashMicroPython + ) + self.__stlinkMenu.addSeparator() + self.__resetAct = self.__stlinkMenu.addAction( + self.tr("Reset Device"), self.__resetDevice + ) + + 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.__stlinkInfoAct.setEnabled(not linkConnected) + self.__flashMpyAct.setEnabled(not linkConnected) + self.__resetAct.setEnabled(connected) + + menu.addMenu(self.__stlinkMenu) + + 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 + + def __stlinkToolAvailable(self, toolname): + """ + Private method to check the availability of the given STLink tool. + + Note: supported tools are st-info and st-flash + + @param toolname name of the tool to be checked + @type str + @return flag indicating the availability of the given STLink tool + @rtype bool + @exception ValueError raised to indicate an illegal tool name + """ + if toolname not in ("st-info", "st-flash"): + raise ValueError("Illegal tool name given.") + + preferencesKey = "StInfoPath" if toolname == "st-info" else "StFlashPath" + + available = False + program = Preferences.getMicroPython(preferencesKey) + if not program: + program = toolname + if FileSystemUtilities.isinpath(program): + available = True + else: + if FileSystemUtilities.isExecutable(program): + available = True + + if not available: + msg = ( + self.tr( + """The STLink information tool <b>st-info</b> cannot be found or""" + """ is not executable. Ensure it is in the search path or""" + """ configure it on the MicroPython configuration page.""" + ) + if toolname == "st-info" + else self.tr( + """The STLink firmware flashing tool <b>st-flash</b> cannot be""" + """ found or is not executable. Ensure it is in the search path""" + """ or configure it on the MicroPython configuration page.""" + ) + ) + EricMessageBox.critical( + self.microPython, + self.tr("{0} not available").format(toolname), + msg, + ) + + return available + + def __stflashAvailable(self): + """ + Private method to check the availability of the 'st-flash' firmware flashing + tool. + + @return flag indicating the availability of the 'st-flash' firmware flashing + tool + @rtype bool + """ + return self.__stlinkToolAvailable("st-flash") + + def __stinfoAvailable(self): + """ + Private method to check the availability of the 'st-info' tool. + + @return flag indicating the availability of the 'st-info' tool + @rtype bool + """ + return self.__stlinkToolAvailable("st-flash") + + @pyqtSlot() + def __flashMicroPython(self): + """ + Private slot to flash a MicroPython firmware. + """ + if self.__stflashAvailable(): + ok2continue = EricMessageBox.question( + None, + self.tr("Flash MicroPython Firmware"), + self.tr( + """Ensure that only one STLink device is connected. Press OK""" + """ to continue.""" + ), + EricMessageBox.Cancel | EricMessageBox.Ok, + EricMessageBox.Cancel, + ) + if ok2continue: + program = Preferences.getMicroPython("StFlashPath") + if not program: + program = "st-flash" + + downloadsPath = QStandardPaths.standardLocations( + QStandardPaths.StandardLocation.DownloadLocation + )[0] + firmware = EricFileDialog.getOpenFileName( + self.microPython, + self.tr("Flash MicroPython Firmware"), + downloadsPath, + self.tr("MicroPython Firmware Files (*.hex *.bin);; All Files (*)"), + ) + if firmware and os.path.exists(firmware): + args = [ + "--connect-under-reset" + ] + if os.path.splitext(firmware)[-1].lower() == ".hex": + args.extend(["--format", "ihex", "write", firmware]) + else: + args.extend(["write", firmware, "0x08000000"]) + dlg = EricProcessDialog( + outputTitle=self.tr("'st-flash' Output"), + windowTitle=self.tr("Flash MicroPython Firmware"), + showInput=False, + combinedOutput=True, + ) + res = dlg.startProcess(program, args) + if res: + dlg.exec() + + @pyqtSlot() + def __showDeviceInfo(self): + """ + Private slot to show some information about connected STLink devices. + """ + if self.__stinfoAvailable(): + program = Preferences.getMicroPython("StInfoPath") + if not program: + program = "st-info" + + dlg = EricProcessDialog( + self.tr("'st-info' Output"), + self.tr("STLink Device Information"), + ) + res = dlg.startProcess(program, ["--probe"]) + 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": + 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.""" + """ Aborting...""" + ), + ) + else: + ui = ericApp().getObject("UserInterface") + request = QNetworkRequest(QUrl(FirmwareGithubUrls["micropython"])) + reply = ui.networkAccessManager().head(request) + reply.finished.connect(lambda: self.__firmwareVersionResponse(reply)) + + @pyqtSlot(QNetworkReply) + def __firmwareVersionResponse(self, reply): + """ + Private slot 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) + + msg = self.tr( + "<h4>MicroPython Version Information</h4>" + "<table>" + "<tr><td>Installed:</td><td>{0}</td></tr>" + "<tr><td>Available:</td><td>{1}</td></tr>" + "</table>" + ).format(currentVersionStr, tag) + if currentVersion < latestVersion: + msg += self.tr("<p><b>Update available!</b></p>") + + EricMessageBox.information( + None, + self.tr("MicroPython Version"), + msg, + ) + + @pyqtSlot() + def __resetDevice(self): + """ + Private slot to reset the connected device. + """ + self.microPython.deviceInterface().execute( + "import machine\nmachine.reset()\n", mode=self._submitMode + ) + + +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 PyBoardDevice + """ + return STLinkDevice(microPythonWidget, deviceType)