src/eric7/MicroPython/MicrobitDevices.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 8881
54e42bc2437a
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2019 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the device interface class for BBC micro:bit and
8 Calliope mini boards.
9 """
10
11 import os
12 import shutil
13
14 from PyQt6.QtCore import pyqtSlot, QStandardPaths
15 from PyQt6.QtWidgets import QInputDialog, QLineEdit
16
17 from .MicroPythonDevices import MicroPythonDevice
18 from .MicroPythonWidget import HAS_QTCHART
19
20 from EricWidgets import EricMessageBox, EricFileDialog
21 from EricWidgets.EricApplication import ericApp
22
23 import Utilities
24 import Preferences
25
26
27 class MicrobitDevice(MicroPythonDevice):
28 """
29 Class implementing the device for BBC micro:bit and Calliope mini boards.
30 """
31 def __init__(self, microPythonWidget, deviceType, parent=None):
32 """
33 Constructor
34
35 @param microPythonWidget reference to the main MicroPython widget
36 @type MicroPythonWidget
37 @param deviceType type of the device
38 @type str
39 @param parent reference to the parent object
40 @type QObject
41 """
42 super().__init__(
43 microPythonWidget, deviceType, parent)
44
45 def setButtons(self):
46 """
47 Public method to enable the supported action buttons.
48 """
49 super().setButtons()
50 self.microPython.setActionButtons(
51 run=True, repl=True, files=True, chart=HAS_QTCHART)
52
53 def forceInterrupt(self):
54 """
55 Public method to determine the need for an interrupt when opening the
56 serial connection.
57
58 @return flag indicating an interrupt is needed
59 @rtype bool
60 """
61 return True
62
63 def deviceName(self):
64 """
65 Public method to get the name of the device.
66
67 @return name of the device
68 @rtype str
69 """
70 if self.getDeviceType() == "bbc_microbit":
71 # BBC micro:bit
72 return self.tr("BBC micro:bit")
73 else:
74 # Calliope mini
75 return self.tr("Calliope mini")
76
77 def canStartRepl(self):
78 """
79 Public method to determine, if a REPL can be started.
80
81 @return tuple containing a flag indicating it is safe to start a REPL
82 and a reason why it cannot.
83 @rtype tuple of (bool, str)
84 """
85 return True, ""
86
87 def canStartPlotter(self):
88 """
89 Public method to determine, if a Plotter can be started.
90
91 @return tuple containing a flag indicating it is safe to start a
92 Plotter and a reason why it cannot.
93 @rtype tuple of (bool, str)
94 """
95 return True, ""
96
97 def canRunScript(self):
98 """
99 Public method to determine, if a script can be executed.
100
101 @return tuple containing a flag indicating it is safe to start a
102 Plotter and a reason why it cannot.
103 @rtype tuple of (bool, str)
104 """
105 return True, ""
106
107 def runScript(self, script):
108 """
109 Public method to run the given Python script.
110
111 @param script script to be executed
112 @type str
113 """
114 pythonScript = script.split("\n")
115 self.sendCommands(pythonScript)
116
117 def canStartFileManager(self):
118 """
119 Public method to determine, if a File Manager can be started.
120
121 @return tuple containing a flag indicating it is safe to start a
122 File Manager and a reason why it cannot.
123 @rtype tuple of (bool, str)
124 """
125 return True, ""
126
127 def hasTimeCommands(self):
128 """
129 Public method to check, if the device supports time commands.
130
131 The default returns True.
132
133 @return flag indicating support for time commands
134 @rtype bool
135 """
136 return False
137
138 def addDeviceMenuEntries(self, menu):
139 """
140 Public method to add device specific entries to the given menu.
141
142 @param menu reference to the context menu
143 @type QMenu
144 """
145 connected = self.microPython.isConnected()
146
147 act = menu.addAction(self.tr("Flash MicroPython"),
148 self.__flashMicroPython)
149 act.setEnabled(not connected)
150 act = menu.addAction(self.tr("Flash Firmware"),
151 lambda: self.__flashMicroPython(firmware=True))
152 act.setEnabled(not connected)
153 menu.addSeparator()
154 act = menu.addAction(self.tr("Save Script"), self.__saveScriptToDevice)
155 act.setToolTip(self.tr(
156 "Save the current script to the selected device"))
157 act.setEnabled(connected)
158 act = menu.addAction(self.tr("Save Script as 'main.py'"),
159 self.__saveMain)
160 act.setToolTip(self.tr(
161 "Save the current script as 'main.py' on the connected device"))
162 act.setEnabled(connected)
163 menu.addSeparator()
164 act = menu.addAction(self.tr("Reset {0}").format(self.deviceName()),
165 self.__resetDevice)
166 act.setEnabled(connected)
167
168 def hasFlashMenuEntry(self):
169 """
170 Public method to check, if the device has its own flash menu entry.
171
172 @return flag indicating a specific flash menu entry
173 @rtype bool
174 """
175 return True
176
177 @pyqtSlot()
178 def __flashMicroPython(self, firmware=False):
179 """
180 Private slot to flash MicroPython or the DAPLink firmware to the
181 device.
182
183 @param firmware flag indicating to flash the DAPLink firmware
184 @type bool
185 """
186 # Attempts to find the path on the file system that represents the
187 # plugged in micro:bit board. To flash the DAPLink firmware, it must be
188 # in maintenance mode, for MicroPython in standard mode.
189 if self.getDeviceType() == "bbc_microbit":
190 # BBC micro:bit
191 if firmware:
192 deviceDirectories = Utilities.findVolume("MAINTENANCE",
193 findAll=True)
194 else:
195 deviceDirectories = Utilities.findVolume("MICROBIT",
196 findAll=True)
197 else:
198 # Calliope mini
199 if firmware:
200 deviceDirectories = Utilities.findVolume("MAINTENANCE",
201 findAll=True)
202 else:
203 deviceDirectories = Utilities.findVolume("MINI",
204 findAll=True)
205 if len(deviceDirectories) == 0:
206 if self.getDeviceType() == "bbc_microbit":
207 # BBC micro:bit is not ready or not mounted
208 if firmware:
209 EricMessageBox.critical(
210 self.microPython,
211 self.tr("Flash MicroPython/Firmware"),
212 self.tr(
213 '<p>The BBC micro:bit is not ready for flashing'
214 ' the DAPLink firmware. Follow these'
215 ' instructions. </p>'
216 '<ul>'
217 '<li>unplug USB cable and any batteries</li>'
218 '<li>keep RESET button pressed an plug USB cable'
219 ' back in</li>'
220 '<li>a drive called MAINTENANCE should be'
221 ' available</li>'
222 '</ul>'
223 '<p>See the '
224 '<a href="https://microbit.org/guide/firmware/">'
225 'micro:bit web site</a> for details.</p>'
226 )
227 )
228 else:
229 EricMessageBox.critical(
230 self.microPython,
231 self.tr("Flash MicroPython/Firmware"),
232 self.tr(
233 '<p>The BBC micro:bit is not ready for flashing'
234 ' the MicroPython firmware. Please make sure,'
235 ' that a drive called MICROBIT is available.'
236 '</p>'
237 )
238 )
239 else:
240 # Calliope mini is not ready or not mounted
241 if firmware:
242 EricMessageBox.critical(
243 self.microPython,
244 self.tr("Flash MicroPython/Firmware"),
245 self.tr(
246 '<p>The "Calliope mini" is not ready for flashing'
247 ' the DAPLink firmware. Follow these'
248 ' instructions. </p>'
249 '<ul>'
250 '<li>unplug USB cable and any batteries</li>'
251 '<li>keep RESET button pressed an plug USB cable'
252 ' back in</li>'
253 '<li>a drive called MAINTENANCE should be'
254 ' available</li>'
255 '</ul>'
256 )
257 )
258 else:
259 EricMessageBox.critical(
260 self.microPython,
261 self.tr("Flash MicroPython/Firmware"),
262 self.tr(
263 '<p>The "Calliope mini" is not ready for flashing'
264 ' the MicroPython firmware. Please make sure,'
265 ' that a drive called MINI is available.'
266 '</p>'
267 )
268 )
269 elif len(deviceDirectories) == 1:
270 downloadsPath = QStandardPaths.standardLocations(
271 QStandardPaths.StandardLocation.DownloadLocation)[0]
272 firmware = EricFileDialog.getOpenFileName(
273 self.microPython,
274 self.tr("Flash MicroPython/Firmware"),
275 downloadsPath,
276 self.tr("MicroPython/Firmware Files (*.hex *.bin);;"
277 "All Files (*)"))
278 if firmware and os.path.exists(firmware):
279 shutil.copy2(firmware, deviceDirectories[0])
280 else:
281 EricMessageBox.warning(
282 self,
283 self.tr("Flash MicroPython/Firmware"),
284 self.tr("There are multiple devices ready for flashing."
285 " Please make sure, that only one device is prepared.")
286 )
287
288 @pyqtSlot()
289 def __saveMain(self):
290 """
291 Private slot to copy the current script as 'main.py' onto the
292 connected device.
293 """
294 self.__saveScriptToDevice("main.py")
295
296 @pyqtSlot()
297 def __saveScriptToDevice(self, scriptName=""):
298 """
299 Private method to save the current script onto the connected
300 device.
301
302 @param scriptName name of the file on the device
303 @type str
304 """
305 aw = ericApp().getObject("ViewManager").activeWindow()
306 if not aw:
307 return
308
309 title = (
310 self.tr("Save Script as '{0}'").format(scriptName)
311 if scriptName else
312 self.tr("Save Script")
313 )
314
315 if not (aw.isPyFile() or aw.isMicroPythonFile()):
316 yes = EricMessageBox.yesNo(
317 self.microPython,
318 title,
319 self.tr("""The current editor does not contain a Python"""
320 """ script. Write it anyway?"""))
321 if not yes:
322 return
323
324 script = aw.text().strip()
325 if not script:
326 EricMessageBox.warning(
327 self.microPython,
328 title,
329 self.tr("""The script is empty. Aborting."""))
330 return
331
332 if not scriptName:
333 scriptName = os.path.basename(aw.getFileName())
334 scriptName, ok = QInputDialog.getText(
335 self.microPython,
336 title,
337 self.tr("Enter a file name on the device:"),
338 QLineEdit.EchoMode.Normal,
339 scriptName)
340 if not ok or not bool(scriptName):
341 return
342
343 title = self.tr("Save Script as '{0}'").format(scriptName)
344
345 commands = [
346 "fd = open('{0}', 'wb')".format(scriptName),
347 "f = fd.write",
348 ]
349 for line in script.splitlines():
350 commands.append("f(" + repr(line + "\n") + ")")
351 commands.append("fd.close()")
352 out, err = self.microPython.commandsInterface().execute(commands)
353 if err:
354 EricMessageBox.critical(
355 self.microPython,
356 title,
357 self.tr("""<p>The script could not be saved to the"""
358 """ device.</p><p>Reason: {0}</p>""")
359 .format(err.decode("utf-8")))
360
361 # reset the device
362 self.__resetDevice()
363
364 @pyqtSlot()
365 def __resetDevice(self):
366 """
367 Private slot to reset the connected device.
368 """
369 if self.getDeviceType() == "bbc_microbit":
370 # BBC micro:bit
371 self.microPython.commandsInterface().execute([
372 "import microbit",
373 "microbit.reset()",
374 ])
375 else:
376 # Calliope mini
377 self.microPython.commandsInterface().execute([
378 "import calliope_mini",
379 "calliope_mini.reset()",
380 ])
381
382 def getDocumentationUrl(self):
383 """
384 Public method to get the device documentation URL.
385
386 @return documentation URL of the device
387 @rtype str
388 """
389 if self.getDeviceType() == "bbc_microbit":
390 # BBC micro:bit
391 return Preferences.getMicroPython("MicrobitDocuUrl")
392 else:
393 # Calliope mini
394 return Preferences.getMicroPython("CalliopeDocuUrl")
395
396 def getDownloadMenuEntries(self):
397 """
398 Public method to retrieve the entries for the downloads menu.
399
400 @return list of tuples with menu text and URL to be opened for each
401 entry
402 @rtype list of tuple of (str, str)
403 """
404 if self.getDeviceType() == "bbc_microbit":
405 return [
406 (self.tr("MicroPython Firmware for BBC micro:bit V1"),
407 Preferences.getMicroPython("MicrobitMicroPythonUrl")),
408 (self.tr("MicroPython Firmware for BBC micro:bit V2"),
409 Preferences.getMicroPython("MicrobitV2MicroPythonUrl")),
410 (self.tr("DAPLink Firmware"),
411 Preferences.getMicroPython("MicrobitFirmwareUrl"))
412 ]
413 else:
414 return [
415 (self.tr("MicroPython Firmware"),
416 Preferences.getMicroPython("CalliopeMicroPythonUrl")),
417 (self.tr("DAPLink Firmware"),
418 Preferences.getMicroPython("CalliopeDAPLinkUrl"))
419 ]

eric ide

mercurial