MicroPython: implemented code to deal with multiple devices attached to the computer.

Wed, 03 Feb 2021 19:15:58 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Wed, 03 Feb 2021 19:15:58 +0100
changeset 8062
8dc5acb30a8b
parent 8061
979562f350bf
child 8063
c2b07b12ad30

MicroPython: implemented code to deal with multiple devices attached to the computer.

eric6/MicroPython/CircuitPythonDevices.py file | annotate | diff | comparison | revisions
eric6/MicroPython/CircuitPythonFirmwareSelectionDialog.py file | annotate | diff | comparison | revisions
eric6/MicroPython/MicroPythonDevices.py file | annotate | diff | comparison | revisions
eric6/MicroPython/MicrobitDevices.py file | annotate | diff | comparison | revisions
eric6/MicroPython/PyBoardDevices.py file | annotate | diff | comparison | revisions
eric6/Utilities/__init__.py file | annotate | diff | comparison | revisions
--- a/eric6/MicroPython/CircuitPythonDevices.py	Wed Feb 03 19:14:35 2021 +0100
+++ b/eric6/MicroPython/CircuitPythonDevices.py	Wed Feb 03 19:15:58 2021 +0100
@@ -38,6 +38,8 @@
         @type QObject
         """
         super(CircuitPythonDevice, self).__init__(microPythonWidget, parent)
+        
+        self.__workspace = self.__findWorkspace()
     
     def setButtons(self):
         """
@@ -135,7 +137,7 @@
         @return flag indicated a mounted device
         @rtype bool
         """
-        return self.getWorkspace(silent=True).endswith(self.DeviceVolumeName)
+        return self.DeviceVolumeName in self.getWorkspace(silent=True)
     
     def getWorkspace(self, silent=False):
         """
@@ -146,12 +148,32 @@
         @return workspace directory used for saving files
         @rtype str
         """
-        # Attempts to find the path on the filesystem that represents the
-        # plugged in CIRCUITPY board.
-        deviceDirectory = Utilities.findVolume(self.DeviceVolumeName)
+        if self.__workspace:
+            # return cached entry
+            return self.__workspace
+        else:
+            self.__workspace = self.__findWorkspace(silent=silent)
+            return self.__workspace
+    
+    def __findWorkspace(self, silent=False):
+        """
+        Public method to find the workspace directory.
         
-        if deviceDirectory:
-            return deviceDirectory
+        @param silent flag indicating silent operations
+        @type bool
+        @return workspace directory used for saving files
+        @rtype str
+        """
+        # Attempts to find the paths on the filesystem that represents the
+        # plugged in CIRCUITPY boards.
+        deviceDirectories = Utilities.findVolume(self.DeviceVolumeName,
+                                                 all=True)
+        
+        if deviceDirectories:
+            if len(deviceDirectories) == 1:
+                return deviceDirectories[0]
+            else:
+                return self.selectDeviceDirectory(deviceDirectories)
         else:
             # return the default workspace and give the user a warning (unless
             # silent mode is selected)
--- a/eric6/MicroPython/CircuitPythonFirmwareSelectionDialog.py	Wed Feb 03 19:14:35 2021 +0100
+++ b/eric6/MicroPython/CircuitPythonFirmwareSelectionDialog.py	Wed Feb 03 19:15:58 2021 +0100
@@ -85,6 +85,7 @@
             ("PyPortal Pynt", "PORTALBOOT"),
             ("PyPortal Titano", "PORTALBOOT"),
             ("PyRuler", "TRINKETBOOT"),
+            ("QT Py M0", "QTPY_BOOT"),
             ("Radiofruit M0", "RADIOBOOT"),
             ("Trellis M4 Express", "TRELM4BOOT"),
             ("Trinket M0", "TRINKETBOOT"),
@@ -103,6 +104,7 @@
             # Seed boards
             ("--- Seeed Studio ---", ""),
             ("Grove Zero", "Grove Zero"),
+            ("Seeduino XIAO", "Arduino"),
             
             # other boards we know about
             (self.tr("--- Others ---"), ""),
@@ -140,9 +142,18 @@
             if volumeName and volumeName != self.__manualMarker:
                 # check if the user selected a board and the board is in
                 # bootloader mode
-                deviceDirectory = Utilities.findVolume(volumeName)
-                if deviceDirectory:
-                    self.bootPicker.setText(deviceDirectory)
+                deviceDirectories = Utilities.findVolume(volumeName, all=True)
+                if len(deviceDirectories) > 1:
+                    enable = False
+                    E5MessageBox.warning(
+                        self,
+                        self.tr("Select Path to Device"),
+                        self.tr("There are multiple devices in 'bootloader'"
+                                " mode and mounted. Please make sure, that"
+                                " only one device is prepared for flashing.")
+                    )
+                elif len(deviceDirectories) == 1:
+                    self.bootPicker.setText(deviceDirectories[0])
                     enable = True
                 else:
                     enable = False
--- a/eric6/MicroPython/MicroPythonDevices.py	Wed Feb 03 19:14:35 2021 +0100
+++ b/eric6/MicroPython/MicroPythonDevices.py	Wed Feb 03 19:15:58 2021 +0100
@@ -12,6 +12,7 @@
 import os
 
 from PyQt5.QtCore import pyqtSlot, QObject
+from PyQt5.QtWidgets import QInputDialog
 
 from E5Gui.E5Application import e5App
 
@@ -58,8 +59,9 @@
             (0x04D8, 0xED94),       # PyCubed
             (0x04D8, 0xEDBE),       # SAM32
             (0x1D50, 0x60E8),       # PewPew Game Console
+            (0x2886, 0x002F),       # Seeed XIAO
             (0x2886, 0x802D),       # Seeed Wio Terminal
-            (0x2886, 0x002F),       # Seeed XIAO
+            (0x2886, 0x802F),       # Seeed XIAO
             (0x1B4F, 0x0016),       # Sparkfun Thing Plus - SAMD51
             (0x2341, 0x8057),       # Arduino Nano 33 IoT board
             (0x04D8, 0xEAD1),       # DynOSSAT-EDU-EPS
@@ -361,6 +363,28 @@
         return (Preferences.getMultiProject("Workspace") or
                 os.path.expanduser("~"))
     
+    def selectDeviceDirectory(self, deviceDirectories):
+        """
+        Public method to select the device directory from a list of detected
+        ones.
+        
+        @param deviceDirectories list of directories to select from
+        @type list of str
+        @return selected directory or an empty string
+        @rtype str
+        """
+        deviceDirectory, ok = QInputDialog.getItem(
+            None,
+            self.tr("Select Device Directory"),
+            self.tr("Select the directory for the connected device:"),
+            [""] + deviceDirectories,
+            0, False)
+        if ok:
+            return deviceDirectory
+        else:
+            # user cancelled
+            return ""
+    
     def sendCommands(self, commandsList):
         """
         Public method to send a list of commands to the device.
--- a/eric6/MicroPython/MicrobitDevices.py	Wed Feb 03 19:14:35 2021 +0100
+++ b/eric6/MicroPython/MicrobitDevices.py	Wed Feb 03 19:15:58 2021 +0100
@@ -42,6 +42,8 @@
         super(MicrobitDevice, self).__init__(microPythonWidget, parent)
         
         self.__deviceType = deviceType
+        
+        self.__workspace = self.__findWorkspace()
     
     def setButtons(self):
         """
@@ -132,17 +134,34 @@
         @return workspace directory used for saving files
         @rtype str
         """
+        if self.__workspace:
+            # return cached entry
+            return self.__workspace
+        else:
+            self.__workspace = self.__findWorkspace()
+            return self.__workspace
+    
+    def __findWorkspace(self):
+        """
+        Public method to find the workspace directory.
+        
+        @return workspace directory used for saving files
+        @rtype str
+        """
         # Attempts to find the path on the filesystem that represents the
         # plugged in MICROBIT or MINI board.
         if self.__deviceType == "bbc_microbit":
             # BBC micro:bit
-            deviceDirectory = Utilities.findVolume("MICROBIT")
+            deviceDirectories = Utilities.findVolume("MICROBIT", all=True)
         else:
             # Calliope mini
-            deviceDirectory = Utilities.findVolume("MINI")
+            deviceDirectories = Utilities.findVolume("MINI", all=True)
         
-        if deviceDirectory:
-            return deviceDirectory
+        if deviceDirectories:
+            if len(deviceDirectories) == 1:
+                return deviceDirectories[0]
+            else:
+                return self.selectDeviceDirectory(deviceDirectories)
         else:
             # return the default workspace and give the user a warning
             E5MessageBox.warning(
@@ -207,20 +226,22 @@
         # Attempts to find the path on the file system that represents the
         # plugged in micro:bit board. To flash the DAPLink firmware, it must be
         # in maintenance mode, for MicroPython in standard mode.
-        # The Calliope mini board must be in standard mode.
         if self.__deviceType == "bbc_microbit":
             # BBC micro:bit
             if firmware:
-                deviceDirectory = Utilities.findVolume("MAINTENANCE")
+                deviceDirectories = Utilities.findVolume("MAINTENANCE",
+                                                         all=True)
             else:
-                deviceDirectory = Utilities.findVolume("MICROBIT")
+                deviceDirectories = Utilities.findVolume("MICROBIT",
+                                                         all=True)
         else:
             # Calliope mini
             if firmware:
-                deviceDirectory = Utilities.findVolume("MAINTENANCE")
+                deviceDirectories = Utilities.findVolume("MAINTENANCE",
+                                                         all=True)
             else:
-                deviceDirectory = Utilities.findVolume("MINI")
-        if not deviceDirectory:
+                deviceDirectories = Utilities.findVolume("MINI", all=True)
+        if len(deviceDirectories) == 0:
             if self.__deviceType == "bbc_microbit":
                 # BBC micro:bit is not ready or not mounted
                 if firmware:
@@ -284,7 +305,7 @@
                             '</p>'
                         )
                     )
-        else:
+        elif len(deviceDirectories) == 1:
             downloadsPath = QStandardPaths.standardLocations(
                 QStandardPaths.DownloadLocation)[0]
             firmware = E5FileDialog.getOpenFileName(
@@ -294,7 +315,14 @@
                 self.tr("MicroPython/Firmware Files (*.hex *.bin);;"
                         "All Files (*)"))
             if firmware and os.path.exists(firmware):
-                shutil.copy2(firmware, deviceDirectory)
+                shutil.copy2(firmware, deviceDirectories[0])
+        else:
+            E5MessageBox.warning(
+                self,
+                self.tr("Flash MicroPython/Firmware"),
+                self.tr("There are multiple devices in ready for flashing."
+                        " Please make sure, that only one device is prepared.")
+            )
     
     @pyqtSlot()
     def __saveMain(self):
--- a/eric6/MicroPython/PyBoardDevices.py	Wed Feb 03 19:14:35 2021 +0100
+++ b/eric6/MicroPython/PyBoardDevices.py	Wed Feb 03 19:15:58 2021 +0100
@@ -43,6 +43,8 @@
         @type QObject
         """
         super(PyBoardDevice, self).__init__(microPythonWidget, parent)
+        
+        self.__workspace = self.__findWorkspace()
     
     def setButtons(self):
         """
@@ -140,7 +142,7 @@
         @return flag indicated a mounted device
         @rtype bool
         """
-        return self.getWorkspace(silent=True).endswith(self.DeviceVolumeName)
+        return self.DeviceVolumeName in self.getWorkspace(silent=True)
     
     def getWorkspace(self, silent=False):
         """
@@ -151,12 +153,32 @@
         @return workspace directory used for saving files
         @rtype str
         """
+        if self.__workspace:
+            # return cached entry
+            return self.__workspace
+        else:
+            self.__workspace = self.__findWorkspace(silent=silent)
+            return self.__workspace
+    
+    def __findWorkspace(self, silent=False):
+        """
+        Public method to find the workspace directory.
+        
+        @param silent flag indicating silent operations
+        @type bool
+        @return workspace directory used for saving files
+        @rtype str
+        """
         # Attempts to find the path on the filesystem that represents the
         # plugged in PyBoard board.
-        deviceDirectory = Utilities.findVolume(self.DeviceVolumeName)
+        deviceDirectories = Utilities.findVolume(self.DeviceVolumeName,
+                                                 all=True)
         
-        if deviceDirectory:
-            return deviceDirectory
+        if deviceDirectories:
+            if len(deviceDirectories) == 1:
+                return deviceDirectories[0]
+            else:
+                return self.selectDeviceDirectory(deviceDirectories)
         else:
             # return the default workspace and give the user a warning (unless
             # silent mode is selected)
--- a/eric6/Utilities/__init__.py	Wed Feb 03 19:14:35 2021 +0100
+++ b/eric6/Utilities/__init__.py	Wed Feb 03 19:15:58 2021 +0100
@@ -1238,15 +1238,22 @@
     return dirs
 
 
-def findVolume(volumeName):
+def findVolume(volumeName, all=False):
     """
     Function to find the directory belonging to a given volume name.
     
     @param volumeName name of the volume to search for
     @type str
-    @return directory path of the given volume name
-    @rtype str
+    @param all flag indicating to get the directories for all volumes
+        starting with the given name (defaults to False)
+    @type bool (optional)
+    @return directory path or list of directory paths for the given volume
+        name
+    @rtype str or list of str
     """
+    # TODO: add option to get all (i.e. all starting with volumeName)
+    #       and return a list of found directories
+    volumeDirectories = []
     volumeDirectory = None
     
     if isWindowsPlatform():
@@ -1277,10 +1284,14 @@
         try:
             for disk in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
                 dirpath = "{0}:\\".format(disk)
-                if (os.path.exists(dirpath) and
-                        getVolumeName(dirpath) == volumeName):
-                    volumeDirectory = dirpath
-                    break
+                if os.path.exists(dirpath):
+                    if all:
+                        if getVolumeName(dirpath).startswith(volumeName):
+                            volumeDirectories.append(dirpath)
+                    else:
+                        if getVolumeName(dirpath) == volumeName:
+                            volumeDirectory = dirpath
+                            break
         finally:
             ctypes.windll.kernel32.SetErrorMode(oldMode)
     else:
@@ -1291,16 +1302,26 @@
                     subprocess.check_output(mountCommand).splitlines()  # secok
                 )
                 mountedVolumes = [x.split()[2] for x in mountOutput]
-                for volume in mountedVolumes:
-                    if volume.decode("utf-8").endswith(volumeName):
-                        volumeDirectory = volume.decode("utf-8")
+                if all:
+                    for volume in mountedVolumes:
+                        if volumeName in volume.decode("utf-8"):
+                            volumeDirectories.append(volume.decode("utf-8"))
+                    if volumeDirectories:
                         break
-                if volumeDirectory:
-                    break
+                else:
+                    for volume in mountedVolumes:
+                        if volume.decode("utf-8").endswith(volumeName):
+                            volumeDirectory = volume.decode("utf-8")
+                            break
+                    if volumeDirectory:
+                        break
             except FileNotFoundError:
                 pass
     
-    return volumeDirectory
+    if all:
+        return volumeDirectories
+    else:
+        return volumeDirectory
 
 
 def getTestFileName(fn):

eric ide

mercurial