eric7/MicroPython/MicroPythonDevices.py

branch
eric7
changeset 8312
800c432b34c8
parent 8259
2bbec88047dd
child 8318
962bce857696
equal deleted inserted replaced
8311:4e8b98454baa 8312:800c432b34c8
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2019 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing some utility functions and the MicroPythonDevice base
8 class.
9 """
10
11 import logging
12 import os
13
14 from PyQt5.QtCore import pyqtSlot, QObject, QCoreApplication
15 from PyQt5.QtWidgets import QInputDialog
16
17 from E5Gui.E5Application import e5App
18
19 import UI.PixmapCache
20 import Preferences
21
22
23 SupportedBoards = {
24 "esp": {
25 "ids": [
26 (0x0403, 0x6001), # M5Stack ESP32 device"),
27 (0x0403, 0x6001), # FT232/FT245 (XinaBox CW01, CW02)
28 (0x0403, 0x6010), # FT2232C/D/L/HL/Q (ESP-WROVER-KIT)
29 (0x0403, 0x6011), # FT4232
30 (0x0403, 0x6014), # FT232H
31 (0x0403, 0x6015), # Sparkfun ESP32
32 (0x0403, 0x601C), # FT4222H
33 (0x10C4, 0xEA60), # CP210x
34 (0x1A86, 0x7523), # HL-340
35 ],
36 "description": "ESP32, ESP8266",
37 "icon": "esp32Device",
38 "port_description": "",
39 },
40
41 "circuitpython": {
42 "ids": [
43 (0x04D8, 0xEAD1), # BH Dynamics DynOSSAT-EDU-EPS-v1.0
44 (0x04D8, 0xEAD2), # BH Dynamics DynOSSAT-EDU-OBC-v1.0
45 (0x04D8, 0xEC44), # maholli PyCubed
46 (0x04D8, 0xEC63), # Kevin Neubauer CircuitBrains Basic
47 (0x04D8, 0xEC64), # Kevin Neubauer CircuitBrains Deluxe
48 (0x04D8, 0xEC72), # XinaBox CC03
49 (0x04D8, 0xEC75), # XinaBox CS11
50 (0x04D8, 0xED5F), # Itaca Innovation uChip CircuitPython
51 (0x04D8, 0xED94), # maholli kicksat-sprite
52 (0x04D8, 0xEDB3), # Capable Robot Programmable USB Hub
53 (0x04D8, 0xEDBE), # maholli SAM32
54 (0x04D8, 0xEE8C), # J&J Studios LLC datum-Distance
55 (0x04D8, 0xEE8D), # J&J Studios LLC datum-IMU
56 (0x04D8, 0xEE8E), # J&J Studios LLC datum-Light
57 (0x04D8, 0xEE8F), # J&J Studios LLC datum-Weather
58 (0x054C, 0x0BC2), # Sony Spresense
59 (0x1209, 0x2017), # Benjamin Shockley Mini SAM M4
60 (0x1209, 0x3252), # Targett Module Clip w/Wroom
61 (0x1209, 0x3253), # Targett Module Clip w/Wrover
62 (0x1209, 0x4D43), # Robotics Masters Robo HAT MM1 M4
63 (0x1209, 0x4DDD), # ODT CP Sapling
64 (0x1209, 0x4DDE), # ODT CP Sapling M0 w/ SPI Flash
65 (0x1209, 0x5BF0), # Foosn Fomu
66 (0x1209, 0x805A), # Electronic Cats BastBLE
67 (0x1209, 0xBAB0), # Electronic Cats Bast WiFi
68 (0x1209, 0xBAB1), # Electronic Cats Meow Meow
69 (0x1209, 0xBAB2), # Electronic Cats CatWAN USBStick
70 (0x1209, 0xBAB3), # Electronic Cats Bast Pro Mini M0
71 (0x1209, 0xBAB6), # Electronic Cats Escornabot Makech
72 (0x1209, 0xBAB8), # Electronic Cats NFC Copy Cat
73 (0x1209, 0xC051), # Betrusted Simmel
74 (0x1209, 0xE3E3), # StackRduino M0 PRO
75 (0x1209, 0xF500), # Silicognition LLC M4-Shim
76 (0x1915, 0xB001), # Makerdiary Pitaya Go
77 (0x1B4F, 0x0015), # SparkFun RedBoard Turbo Board
78 (0x1B4F, 0x0016), # SparkFun SAMD51 Thing+
79 (0x1B4F, 0x0017), # SparkFun LUMIDrive Board
80 (0x1B4F, 0x5289), # SparkFun SFE_nRF52840_Mini
81 (0x1B4F, 0x8D22), # SparkFun SAMD21 Mini Breakout
82 (0x1B4F, 0x8D23), # SparkFun SAMD21 Dev Breakout
83 (0x1B4F, 0x8D24), # SparkFun Qwiic Micro
84 (0x1D50, 0x60E8), # Radomir Dopieralski PewPew M4
85 (0x2341, 0x8053), # Arduino MKR1300
86 (0x2341, 0x8057), # Arduino Nano 33 IoT
87 (0x2341, 0x805A), # Arduino Arduino_Nano_33_BLE
88 (0x2341, 0x824D), # Arduino Zero
89 (0x2786, 0x9207), # Switch Sc. BLE-SS dev board Multi Sensor
90 (0x2886, 0x002F), # Seeed Seeeduino XIAO
91 (0x2886, 0x802D), # Seeed Seeeduino Wio Terminal
92 (0x2886, 0xF001), # Makerdiary nRF52840 M.2 Developer Kit
93 (0x2886, 0xF002), # Makerdiary M60 Keyboard
94 (0x2B04, 0xC00C), # Particle Argon
95 (0x2B04, 0xC00D), # Particle Boron
96 (0x2B04, 0xC00E), # Particle Xenon
97 (0x303A, 0x8007), # LILYGO TTGO T8 ESP32-S2
98 (0x3171, 0x0101), # 8086 Consultancy Commander
99 (0x31E2, 0x2001), # BDMICRO LLC VINA-D21
100 (0x31E2, 0x2011), # BDMICRO LLC VINA-D51
101 (0x32BD, 0x3001), # Alorium Tech. AloriumTech Evo M51
102 (0x4097, 0x0001), # TG-Boards Datalore IP M4
103
104 (0x239A, None), # Any Adafruit Boards
105 ],
106 "description": "CircuitPython",
107 "icon": "circuitPythonDevice",
108 "port_description": "",
109 },
110
111 "bbc_microbit": {
112 "ids": [
113 (0x0D28, 0x0204), # micro:bit
114 ],
115 "description": "BBC micro:bit",
116 "icon": "microbitDevice",
117 "port_description": "BBC micro:bit CMSIS-DAP",
118 },
119
120 "calliope": {
121 "ids": [
122 (0x0D28, 0x0204), # Calliope mini
123 ],
124 "description": "Calliope mini",
125 "icon": "calliope_mini",
126 "port_description": "DAPLink CMSIS-DAP",
127 },
128
129 "pyboard": {
130 "ids": [
131 (0xF055, 0x9800), # Pyboard in CDC mode
132 (0xF055, 0x9801), # Pyboard in CDC+HID mode
133 (0xF055, 0x9802), # Pyboard in CDC+MSC mode
134 ],
135 "description": "PyBoard",
136 "icon": "micropython48",
137 "port_description": "",
138 },
139
140 "rp2040": {
141 "ids": [
142 (0x2E8A, 0x0005), # Raspberry Pi Pico
143 ],
144 "description": QCoreApplication.translate(
145 "MicroPythonDevice", "RP2040 based"),
146 "icon": "rp2040Device",
147 "port_description": "",
148 },
149
150 "generic": {
151 # only manually configured devices use this
152 "ids": [],
153 "description": QCoreApplication.translate(
154 "MicroPythonDevice", "Generic Board"),
155 "icon": "micropython48",
156 "port_description": "",
157 },
158 }
159
160 IgnoredBoards = (
161 (0x8086, 0x9c3d),
162 )
163
164
165 def getSupportedDevices():
166 """
167 Function to get a list of supported MicroPython devices.
168
169 @return set of tuples with the board type and description
170 @rtype set of tuples of (str, str)
171 """
172 boards = []
173 for board in SupportedBoards:
174 boards.append(
175 (board, SupportedBoards[board]["description"]))
176 return boards
177
178
179 def getFoundDevices():
180 """
181 Function to check the serial ports for supported MicroPython devices.
182
183 @return tuple containing a list of tuples with the board type, the port
184 description, a description, the serial port it is connected at, the
185 VID and PID for known device types, a list of tuples with VID, PID
186 and description for unknown devices and a list of tuples with VID,
187 PID, description and port name for ports with missing VID or PID
188 @rtype tuple of (list of tuples of (str, str, str, str, int, int),
189 list of tuples of (int, int, str),
190 list of tuples of (int, int, str, str)
191 """
192 from PyQt5.QtSerialPort import QSerialPortInfo
193
194 foundDevices = []
195 unknownDevices = []
196 unknownPorts = []
197
198 manualDevices = {}
199 for deviceDict in Preferences.getMicroPython("ManualDevices"):
200 manualDevices[(deviceDict["vid"], deviceDict["pid"])] = deviceDict
201
202 availablePorts = QSerialPortInfo.availablePorts()
203 for port in availablePorts:
204 supported = False
205 vid = port.vendorIdentifier()
206 pid = port.productIdentifier()
207
208 if not port.isValid():
209 # no device detected at port
210 continue
211
212 for board in SupportedBoards:
213 if (
214 (vid, pid) in SupportedBoards[board]["ids"] or
215 (vid, None) in SupportedBoards[board]["ids"]
216 ):
217 if (
218 board in ("bbc_microbit", "calliope") and
219 (port.description().strip() !=
220 SupportedBoards[board]["port_description"])
221 ):
222 # both boards have the same VID and PID
223 # try to differentiate based on port description
224 continue
225 foundDevices.append((
226 board,
227 port.description(),
228 SupportedBoards[board]["description"],
229 port.portName(),
230 vid,
231 pid,
232 ))
233 supported = True
234 if not supported and (vid, pid) in manualDevices:
235 # check the locally added ones next
236 board = manualDevices[(vid, pid)]["type"]
237 foundDevices.append((
238 board,
239 port.description(),
240 SupportedBoards[board]["description"],
241 port.portName(),
242 vid,
243 pid,
244 ))
245 supported = True
246 if not supported:
247 if vid and pid:
248 if (vid, pid) not in IgnoredBoards:
249 unknownDevices.append((vid, pid, port.description()))
250 logging.debug("Unknown device: (0x%04x:0x%04x %s)",
251 vid, pid, port.description())
252 else:
253 # either VID or PID or both not detected
254 desc = port.description()
255 if not desc:
256 desc = QCoreApplication.translate("MicroPythonDevice",
257 "Unknown Device")
258 unknownPorts.append((vid, pid, desc, port.portName()))
259
260 return foundDevices, unknownDevices, unknownPorts
261
262
263 def getDeviceIcon(boardName, iconFormat=True):
264 """
265 Function to get the icon for the given board.
266
267 @param boardName name of the board
268 @type str
269 @param iconFormat flag indicating to get an icon or a pixmap
270 @type bool
271 @return icon for the board (iconFormat == True) or
272 a pixmap (iconFormat == False)
273 @rtype QIcon or QPixmap
274 """
275 iconName = (
276 SupportedBoards[boardName]["icon"]
277 if boardName in SupportedBoards else
278 # return a generic MicroPython icon
279 "micropython48"
280 )
281
282 if iconFormat:
283 return UI.PixmapCache.getIcon(iconName)
284 else:
285 return UI.PixmapCache.getPixmap(iconName)
286
287
288 def getDevice(deviceType, microPythonWidget, vid, pid):
289 """
290 Public method to instantiate a specific MicroPython device interface.
291
292 @param deviceType type of the device interface
293 @type str
294 @param microPythonWidget reference to the main MicroPython widget
295 @type MicroPythonWidget
296 @param vid vendor ID (only used for deviceType 'generic')
297 @type int
298 @param pid product ID (only used for deviceType 'generic')
299 @type int
300 @return instantiated device interface
301 @rtype MicroPythonDevice
302 """
303 if deviceType == "esp":
304 from .EspDevices import EspDevice
305 return EspDevice(microPythonWidget, deviceType)
306 elif deviceType == "circuitpython":
307 from .CircuitPythonDevices import CircuitPythonDevice
308 return CircuitPythonDevice(microPythonWidget, deviceType)
309 elif deviceType in ("bbc_microbit", "calliope"):
310 from .MicrobitDevices import MicrobitDevice
311 return MicrobitDevice(microPythonWidget, deviceType)
312 elif deviceType == "pyboard":
313 from .PyBoardDevices import PyBoardDevice
314 return PyBoardDevice(microPythonWidget, deviceType)
315 elif deviceType == "rp2040":
316 from .RP2040Devices import RP2040Device
317 return RP2040Device(microPythonWidget, deviceType)
318 elif deviceType == "generic":
319 from .GenericMicroPythonDevices import GenericMicroPythonDevice
320 return GenericMicroPythonDevice(microPythonWidget, deviceType,
321 vid, pid)
322 else:
323 # nothing specific requested
324 return MicroPythonDevice(microPythonWidget, deviceType)
325
326
327 class MicroPythonDevice(QObject):
328 """
329 Base class for the more specific MicroPython devices.
330 """
331 def __init__(self, microPythonWidget, deviceType, parent=None):
332 """
333 Constructor
334
335 @param microPythonWidget reference to the main MicroPython widget
336 @type MicroPythonWidget
337 @param deviceType device type assigned to this device interface
338 @type str
339 @param parent reference to the parent object
340 @type QObject
341 """
342 super().__init__(parent)
343
344 self._deviceType = deviceType
345 self.microPython = microPythonWidget
346
347 def getDeviceType(self):
348 """
349 Public method to get the device type.
350
351 @return type of the device
352 @rtype str
353 """
354 return self._deviceType
355
356 def setButtons(self):
357 """
358 Public method to enable the supported action buttons.
359 """
360 self.microPython.setActionButtons(
361 open=False, save=False,
362 run=False, repl=False, files=False, chart=False)
363
364 def forceInterrupt(self):
365 """
366 Public method to determine the need for an interrupt when opening the
367 serial connection.
368
369 @return flag indicating an interrupt is needed
370 @rtype bool
371 """
372 return True
373
374 def deviceName(self):
375 """
376 Public method to get the name of the device.
377
378 @return name of the device
379 @rtype str
380 """
381 return self.tr("Unsupported Device")
382
383 def canStartRepl(self):
384 """
385 Public method to determine, if a REPL can be started.
386
387 @return tuple containing a flag indicating it is safe to start a REPL
388 and a reason why it cannot.
389 @rtype tuple of (bool, str)
390 """
391 return False, self.tr("REPL is not supported by this device.")
392
393 def setRepl(self, on):
394 """
395 Public method to set the REPL status and dependent status.
396
397 @param on flag indicating the active status
398 @type bool
399 """
400 pass
401
402 def canStartPlotter(self):
403 """
404 Public method to determine, if a Plotter can be started.
405
406 @return tuple containing a flag indicating it is safe to start a
407 Plotter and a reason why it cannot.
408 @rtype tuple of (bool, str)
409 """
410 return False, self.tr("Plotter is not supported by this device.")
411
412 def setPlotter(self, on):
413 """
414 Public method to set the Plotter status and dependent status.
415
416 @param on flag indicating the active status
417 @type bool
418 """
419 pass
420
421 def canRunScript(self):
422 """
423 Public method to determine, if a script can be executed.
424
425 @return tuple containing a flag indicating it is safe to start a
426 Plotter and a reason why it cannot.
427 @rtype tuple of (bool, str)
428 """
429 return False, self.tr("Running scripts is not supported by this"
430 " device.")
431
432 def runScript(self, script):
433 """
434 Public method to run the given Python script.
435
436 @param script script to be executed
437 @type str
438 """
439 pass
440
441 def canStartFileManager(self):
442 """
443 Public method to determine, if a File Manager can be started.
444
445 @return tuple containing a flag indicating it is safe to start a
446 File Manager and a reason why it cannot.
447 @rtype tuple of (bool, str)
448 """
449 return False, self.tr("File Manager is not supported by this device.")
450
451 def setFileManager(self, on):
452 """
453 Public method to set the File Manager status and dependent status.
454
455 @param on flag indicating the active status
456 @type bool
457 """
458 pass
459
460 def supportsLocalFileAccess(self):
461 """
462 Public method to indicate file access via a local directory.
463
464 @return flag indicating file access via local directory
465 @rtype bool
466 """
467 return False # default
468
469 def getWorkspace(self):
470 """
471 Public method to get the workspace directory.
472
473 @return workspace directory used for saving files
474 @rtype str
475 """
476 return (
477 Preferences.getMicroPython("MpyWorkspace") or
478 Preferences.getMultiProject("Workspace") or
479 os.path.expanduser("~")
480 )
481
482 def selectDeviceDirectory(self, deviceDirectories):
483 """
484 Public method to select the device directory from a list of detected
485 ones.
486
487 @param deviceDirectories list of directories to select from
488 @type list of str
489 @return selected directory or an empty string
490 @rtype str
491 """
492 deviceDirectory, ok = QInputDialog.getItem(
493 None,
494 self.tr("Select Device Directory"),
495 self.tr("Select the directory for the connected device:"),
496 [""] + deviceDirectories,
497 0, False)
498 if ok:
499 return deviceDirectory
500 else:
501 # user cancelled
502 return ""
503
504 def sendCommands(self, commandsList):
505 """
506 Public method to send a list of commands to the device.
507
508 @param commandsList list of commands to be sent to the device
509 @type list of str
510 """
511 rawOn = [ # sequence of commands to enter raw mode
512 b'\x02', # Ctrl-B: exit raw repl (just in case)
513 b'\r\x03\x03\x03', # Ctrl-C three times: interrupt any running
514 # program
515 b'\r\x01', # Ctrl-A: enter raw REPL
516 ]
517 newLine = [b'print("\\n")\r', ]
518 commands = [c.encode("utf-8)") + b'\r' for c in commandsList]
519 commands.append(b'\r')
520 commands.append(b'\x04')
521 rawOff = [b'\x02', b'\x02']
522 commandSequence = rawOn + newLine + commands + rawOff
523 self.microPython.commandsInterface().executeAsync(commandSequence)
524
525 @pyqtSlot()
526 def handleDataFlood(self):
527 """
528 Public slot handling a data floof from the device.
529 """
530 pass
531
532 def addDeviceMenuEntries(self, menu):
533 """
534 Public method to add device specific entries to the given menu.
535
536 @param menu reference to the context menu
537 @type QMenu
538 """
539 pass
540
541 def hasFlashMenuEntry(self):
542 """
543 Public method to check, if the device has its own flash menu entry.
544
545 @return flag indicating a specific flash menu entry
546 @rtype bool
547 """
548 return False
549
550 def hasTimeCommands(self):
551 """
552 Public method to check, if the device supports time commands.
553
554 The default returns True.
555
556 @return flag indicating support for time commands
557 @rtype bool
558 """
559 return True
560
561 def hasDocumentationUrl(self):
562 """
563 Public method to check, if the device has a configured documentation
564 URL.
565
566 @return flag indicating a configured documentation URL
567 @rtype bool
568 """
569 return bool(self.getDocumentationUrl())
570
571 def getDocumentationUrl(self):
572 """
573 Public method to get the device documentation URL.
574
575 @return documentation URL of the device
576 @rtype str
577 """
578 return ""
579
580 def hasFirmwareUrl(self):
581 """
582 Public method to check, if the device has a configured firmware
583 download URL.
584
585 @return flag indicating a configured firmware download URL
586 @rtype bool
587 """
588 return bool(self.getFirmwareUrl())
589
590 def getFirmwareUrl(self):
591 """
592 Public method to get the device firmware download URL.
593
594 @return firmware download URL of the device
595 @rtype str
596 """
597 return ""
598
599 def downloadFirmware(self):
600 """
601 Public method to download the device firmware.
602 """
603 url = self.getFirmwareUrl()
604 if url:
605 e5App().getObject("UserInterface").launchHelpViewer(url)
606
607 def getDownloadMenuEntries(self):
608 """
609 Public method to retrieve the entries for the downloads menu.
610
611 @return list of tuples with menu text and URL to be opened for each
612 entry
613 @rtype list of tuple of (str, str)
614 """
615 return []

eric ide

mercurial