src/eric7/MicroPython/Devices/STLinkDevices.py

branch
eric7
changeset 9958
a78b83d1062a
child 9972
68ac01294544
equal deleted inserted replaced
9957:0457d754fc9a 9958:a78b83d1062a
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2023 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the device interface class for STM32 STLink boards.
8 """
9
10 import os
11
12 from PyQt6.QtCore import QStandardPaths, QUrl, pyqtSlot
13 from PyQt6.QtNetwork import QNetworkReply, QNetworkRequest
14 from PyQt6.QtWidgets import QMenu
15
16 from eric7 import Globals, Preferences
17 from eric7.EricWidgets import EricFileDialog, EricMessageBox
18 from eric7.EricWidgets.EricApplication import ericApp
19 from eric7.EricWidgets.EricProcessDialog import EricProcessDialog
20 from eric7.SystemUtilities import FileSystemUtilities
21
22 from ..MicroPythonWidget import HAS_QTCHART
23 from . import FirmwareGithubUrls
24 from .DeviceBase import BaseDevice
25
26
27 class STLinkDevice(BaseDevice):
28 """
29 Class implementing the device for PyBoard boards.
30 """
31
32 DeviceVolumeName = "NODE_"
33
34 def __init__(self, microPythonWidget, deviceType, parent=None):
35 """
36 Constructor
37
38 @param microPythonWidget reference to the main MicroPython widget
39 @type MicroPythonWidget
40 @param deviceType device type assigned to this device interface
41 @type str
42 @param parent reference to the parent object
43 @type QObject
44 """
45 super().__init__(microPythonWidget, deviceType, parent)
46
47 self._submitMode = "paste" # use 'paste' mode
48
49 self.__workspace = self.__findWorkspace()
50
51 self.__createSTLinkMenu()
52
53 def setButtons(self):
54 """
55 Public method to enable the supported action buttons.
56 """
57 super().setButtons()
58
59 self.microPython.setActionButtons(
60 run=True, repl=True, files=True, chart=HAS_QTCHART
61 )
62
63 def forceInterrupt(self):
64 """
65 Public method to determine the need for an interrupt when opening the
66 serial connection.
67
68 @return flag indicating an interrupt is needed
69 @rtype bool
70 """
71 return False
72
73 def deviceName(self):
74 """
75 Public method to get the name of the device.
76
77 @return name of the device
78 @rtype str
79 """
80 return self.tr("STM32 STLink")
81
82 def canStartRepl(self):
83 """
84 Public method to determine, if a REPL can be started.
85
86 @return tuple containing a flag indicating it is safe to start a REPL
87 and a reason why it cannot.
88 @rtype tuple of (bool, str)
89 """
90 return True, ""
91
92 def canStartPlotter(self):
93 """
94 Public method to determine, if a Plotter can be started.
95
96 @return tuple containing a flag indicating it is safe to start a
97 Plotter and a reason why it cannot.
98 @rtype tuple of (bool, str)
99 """
100 return True, ""
101
102 def canRunScript(self):
103 """
104 Public method to determine, if a script can be executed.
105
106 @return tuple containing a flag indicating it is safe to start a
107 Plotter and a reason why it cannot.
108 @rtype tuple of (bool, str)
109 """
110 return True, ""
111
112 def runScript(self, script):
113 """
114 Public method to run the given Python script.
115
116 @param script script to be executed
117 @type str
118 """
119 pythonScript = script.split("\n")
120 self.sendCommands(pythonScript)
121
122 def canStartFileManager(self):
123 """
124 Public method to determine, if a File Manager can be started.
125
126 @return tuple containing a flag indicating it is safe to start a
127 File Manager and a reason why it cannot.
128 @rtype tuple of (bool, str)
129 """
130 return True, ""
131
132 def supportsLocalFileAccess(self):
133 """
134 Public method to indicate file access via a local directory.
135
136 @return flag indicating file access via local directory
137 @rtype bool
138 """
139 return self.__deviceVolumeMounted()
140
141 def __deviceVolumeMounted(self):
142 """
143 Private method to check, if the device volume is mounted.
144
145 @return flag indicated a mounted device
146 @rtype bool
147 """
148 if self.__workspace and not os.path.exists(self.__workspace):
149 self.__workspace = "" # reset
150
151 return self.DeviceVolumeName in self.getWorkspace(silent=True)
152
153 def getWorkspace(self, silent=False):
154 """
155 Public method to get the workspace directory.
156
157 @param silent flag indicating silent operations
158 @type bool
159 @return workspace directory used for saving files
160 @rtype str
161 """
162 if self.__workspace:
163 # return cached entry
164 return self.__workspace
165 else:
166 self.__workspace = self.__findWorkspace(silent=silent)
167 return self.__workspace
168
169 def __findWorkspace(self, silent=False):
170 """
171 Private method to find the workspace directory.
172
173 @param silent flag indicating silent operations
174 @type bool
175 @return workspace directory used for saving files
176 @rtype str
177 """
178 # Attempts to find the path on the filesystem that represents the
179 # plugged in PyBoard board.
180 deviceDirectories = FileSystemUtilities.findVolume(
181 self.DeviceVolumeName, findAll=True
182 )
183
184 if deviceDirectories:
185 if len(deviceDirectories) == 1:
186 return deviceDirectories[0]
187 else:
188 return self.selectDeviceDirectory(deviceDirectories)
189 else:
190 # return the default workspace and give the user a warning (unless
191 # silent mode is selected)
192 if not silent:
193 EricMessageBox.warning(
194 self.microPython,
195 self.tr("Workspace Directory"),
196 self.tr(
197 "Python files for STLink boards can be edited in"
198 " place, if the device volume is locally"
199 " available. Such a volume was not found. In"
200 " place editing will not be available."
201 ),
202 )
203
204 return super().getWorkspace()
205
206 def getDocumentationUrl(self):
207 """
208 Public method to get the device documentation URL.
209
210 @return documentation URL of the device
211 @rtype str
212 """
213 return Preferences.getMicroPython("MicroPythonDocuUrl")
214
215 def getFirmwareUrl(self):
216 """
217 Public method to get the device firmware download URL.
218
219 @return firmware download URL of the device
220 @rtype str
221 """
222 return Preferences.getMicroPython("MicroPythonFirmwareUrl")
223
224 def __createSTLinkMenu(self):
225 """
226 Private method to create the STLink submenu.
227 """
228 self.__stlinkMenu = QMenu(self.tr("STLink Functions"))
229
230 self.__showMpyAct = self.__stlinkMenu.addAction(
231 self.tr("Show MicroPython Versions"), self.__showFirmwareVersions
232 )
233 self.__stlinkMenu.addSeparator()
234 self.__stlinkInfoAct = self.__stlinkMenu.addAction(
235 self.tr("Show STLink Device Information"), self.__showDeviceInfo
236 )
237 self.__stlinkMenu.addSeparator()
238 self.__flashMpyAct = self.__stlinkMenu.addAction(
239 self.tr("Flash MicroPython Firmware"), self.__flashMicroPython
240 )
241 self.__stlinkMenu.addSeparator()
242 self.__resetAct = self.__stlinkMenu.addAction(
243 self.tr("Reset Device"), self.__resetDevice
244 )
245
246 def addDeviceMenuEntries(self, menu):
247 """
248 Public method to add device specific entries to the given menu.
249
250 @param menu reference to the context menu
251 @type QMenu
252 """
253 connected = self.microPython.isConnected()
254 linkConnected = self.microPython.isLinkConnected()
255
256 self.__showMpyAct.setEnabled(connected)
257 self.__stlinkInfoAct.setEnabled(not linkConnected)
258 self.__flashMpyAct.setEnabled(not linkConnected)
259 self.__resetAct.setEnabled(connected)
260
261 menu.addMenu(self.__stlinkMenu)
262
263 def hasFlashMenuEntry(self):
264 """
265 Public method to check, if the device has its own flash menu entry.
266
267 @return flag indicating a specific flash menu entry
268 @rtype bool
269 """
270 return True
271
272 def __stlinkToolAvailable(self, toolname):
273 """
274 Private method to check the availability of the given STLink tool.
275
276 Note: supported tools are st-info and st-flash
277
278 @param toolname name of the tool to be checked
279 @type str
280 @return flag indicating the availability of the given STLink tool
281 @rtype bool
282 @exception ValueError raised to indicate an illegal tool name
283 """
284 if toolname not in ("st-info", "st-flash"):
285 raise ValueError("Illegal tool name given.")
286
287 preferencesKey = "StInfoPath" if toolname == "st-info" else "StFlashPath"
288
289 available = False
290 program = Preferences.getMicroPython(preferencesKey)
291 if not program:
292 program = toolname
293 if FileSystemUtilities.isinpath(program):
294 available = True
295 else:
296 if FileSystemUtilities.isExecutable(program):
297 available = True
298
299 if not available:
300 msg = (
301 self.tr(
302 """The STLink information tool <b>st-info</b> cannot be found or"""
303 """ is not executable. Ensure it is in the search path or"""
304 """ configure it on the MicroPython configuration page."""
305 )
306 if toolname == "st-info"
307 else self.tr(
308 """The STLink firmware flashing tool <b>st-flash</b> cannot be"""
309 """ found or is not executable. Ensure it is in the search path"""
310 """ or configure it on the MicroPython configuration page."""
311 )
312 )
313 EricMessageBox.critical(
314 self.microPython,
315 self.tr("{0} not available").format(toolname),
316 msg,
317 )
318
319 return available
320
321 def __stflashAvailable(self):
322 """
323 Private method to check the availability of the 'st-flash' firmware flashing
324 tool.
325
326 @return flag indicating the availability of the 'st-flash' firmware flashing
327 tool
328 @rtype bool
329 """
330 return self.__stlinkToolAvailable("st-flash")
331
332 def __stinfoAvailable(self):
333 """
334 Private method to check the availability of the 'st-info' tool.
335
336 @return flag indicating the availability of the 'st-info' tool
337 @rtype bool
338 """
339 return self.__stlinkToolAvailable("st-flash")
340
341 @pyqtSlot()
342 def __flashMicroPython(self):
343 """
344 Private slot to flash a MicroPython firmware.
345 """
346 if self.__stflashAvailable():
347 ok2continue = EricMessageBox.question(
348 None,
349 self.tr("Flash MicroPython Firmware"),
350 self.tr(
351 """Ensure that only one STLink device is connected. Press OK"""
352 """ to continue."""
353 ),
354 EricMessageBox.Cancel | EricMessageBox.Ok,
355 EricMessageBox.Cancel,
356 )
357 if ok2continue:
358 program = Preferences.getMicroPython("StFlashPath")
359 if not program:
360 program = "st-flash"
361
362 downloadsPath = QStandardPaths.standardLocations(
363 QStandardPaths.StandardLocation.DownloadLocation
364 )[0]
365 firmware = EricFileDialog.getOpenFileName(
366 self.microPython,
367 self.tr("Flash MicroPython Firmware"),
368 downloadsPath,
369 self.tr("MicroPython Firmware Files (*.hex *.bin);; All Files (*)"),
370 )
371 if firmware and os.path.exists(firmware):
372 args = [
373 "--connect-under-reset"
374 ]
375 if os.path.splitext(firmware)[-1].lower() == ".hex":
376 args.extend(["--format", "ihex", "write", firmware])
377 else:
378 args.extend(["write", firmware, "0x08000000"])
379 dlg = EricProcessDialog(
380 outputTitle=self.tr("'st-flash' Output"),
381 windowTitle=self.tr("Flash MicroPython Firmware"),
382 showInput=False,
383 combinedOutput=True,
384 )
385 res = dlg.startProcess(program, args)
386 if res:
387 dlg.exec()
388
389 @pyqtSlot()
390 def __showDeviceInfo(self):
391 """
392 Private slot to show some information about connected STLink devices.
393 """
394 if self.__stinfoAvailable():
395 program = Preferences.getMicroPython("StInfoPath")
396 if not program:
397 program = "st-info"
398
399 dlg = EricProcessDialog(
400 self.tr("'st-info' Output"),
401 self.tr("STLink Device Information"),
402 )
403 res = dlg.startProcess(program, ["--probe"])
404 if res:
405 dlg.exec()
406
407 @pyqtSlot()
408 def __showFirmwareVersions(self):
409 """
410 Private slot to show the firmware version of the connected device and the
411 available firmware version.
412 """
413 if self.microPython.isConnected():
414 if self._deviceData["mpy_name"] != "micropython":
415 EricMessageBox.critical(
416 None,
417 self.tr("Show MicroPython Versions"),
418 self.tr(
419 """The firmware of the connected device cannot be"""
420 """ determined or the board does not run MicroPython."""
421 """ Aborting..."""
422 ),
423 )
424 else:
425 ui = ericApp().getObject("UserInterface")
426 request = QNetworkRequest(QUrl(FirmwareGithubUrls["micropython"]))
427 reply = ui.networkAccessManager().head(request)
428 reply.finished.connect(lambda: self.__firmwareVersionResponse(reply))
429
430 @pyqtSlot(QNetworkReply)
431 def __firmwareVersionResponse(self, reply):
432 """
433 Private slot handling the response of the latest version request.
434
435 @param reply reference to the reply object
436 @type QNetworkReply
437 """
438 latestUrl = reply.url().toString()
439 tag = latestUrl.rsplit("/", 1)[-1]
440 while tag and not tag[0].isdecimal():
441 # get rid of leading non-decimal characters
442 tag = tag[1:]
443 latestVersion = Globals.versionToTuple(tag)
444
445 if self._deviceData["mpy_version"] == "unknown":
446 currentVersionStr = self.tr("unknown")
447 currentVersion = (0, 0, 0)
448 else:
449 currentVersionStr = self._deviceData["mpy_version"]
450 currentVersion = Globals.versionToTuple(currentVersionStr)
451
452 msg = self.tr(
453 "<h4>MicroPython Version Information</h4>"
454 "<table>"
455 "<tr><td>Installed:</td><td>{0}</td></tr>"
456 "<tr><td>Available:</td><td>{1}</td></tr>"
457 "</table>"
458 ).format(currentVersionStr, tag)
459 if currentVersion < latestVersion:
460 msg += self.tr("<p><b>Update available!</b></p>")
461
462 EricMessageBox.information(
463 None,
464 self.tr("MicroPython Version"),
465 msg,
466 )
467
468 @pyqtSlot()
469 def __resetDevice(self):
470 """
471 Private slot to reset the connected device.
472 """
473 self.microPython.deviceInterface().execute(
474 "import machine\nmachine.reset()\n", mode=self._submitMode
475 )
476
477
478 def createDevice(microPythonWidget, deviceType, vid, pid, boardName, serialNumber):
479 """
480 Function to instantiate a MicroPython device object.
481
482 @param microPythonWidget reference to the main MicroPython widget
483 @type MicroPythonWidget
484 @param deviceType device type assigned to this device interface
485 @type str
486 @param vid vendor ID
487 @type int
488 @param pid product ID
489 @type int
490 @param boardName name of the board
491 @type str
492 @param serialNumber serial number of the board
493 @type str
494 @return reference to the instantiated device object
495 @rtype PyBoardDevice
496 """
497 return STLinkDevice(microPythonWidget, deviceType)

eric ide

mercurial