|
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) |