src/eric7/MicroPython/MicrobitDevices.py

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

eric ide

mercurial