【问题标题】:How to open a serial port by friendly name?如何通过友好名称打开串口?
【发布时间】:2016-11-30 03:35:02
【问题描述】:

友好名称 = 出现在“设备管理器”中“端口(COM 和 LPT)”下的名称。

编辑:下面提供了两种解决方案。一个使用 WMI,另一个使用 SetupAPI。

【问题讨论】:

  • 喜欢“COM1”、“COM2”等?或“Aten USB 串行 (COM1)”。如果前者那么 SerialPort1.Portname = "COM1" 那么 SerialPort1.open
  • WMI - "root\CIMV2" 中的两个查询。 1) “SELECT * FROM Win32_SerialPort”或 2) “SELECT * FROM Win32_PnPEntity WHERE Con​​figManagerErrorCode = 0”。正如建议的那样,使用 WMICodeCreator 来了解细节。需要扫描查询结果。

标签: c# serial-port


【解决方案1】:

贴出今晚的代码,供大家欣赏:

public class SetupDiWrap
{
    static public string ComPortNameFromFriendlyNamePrefix(string friendlyNamePrefix)
    {
        const string className = "Ports";
        Guid[] guids = GetClassGUIDs(className);

        System.Text.RegularExpressions.Regex friendlyNameToComPort =
            new System.Text.RegularExpressions.Regex(@".? \((COM\d+)\)$");  // "..... (COMxxx)" -> COMxxxx

        foreach (Guid guid in guids)
        {
            // We start at the "root" of the device tree and look for all
            // devices that match the interface GUID of a disk
            Guid guidClone = guid;
            IntPtr h = SetupDiGetClassDevs(ref guidClone, IntPtr.Zero, IntPtr.Zero, DIGCF_PRESENT | DIGCF_PROFILE);
            if (h.ToInt32() != INVALID_HANDLE_VALUE)
            {
                int nDevice = 0;
                while (true)
                {
                    SP_DEVINFO_DATA da = new SP_DEVINFO_DATA();
                    da.cbSize = (uint)Marshal.SizeOf(da);

                    if (0 == SetupDiEnumDeviceInfo(h, nDevice++, ref da))
                        break;

                    uint RegType;
                    byte[] ptrBuf = new byte[BUFFER_SIZE];
                    uint RequiredSize;
                    if (SetupDiGetDeviceRegistryProperty(h, ref da,
                        (uint)SPDRP.FRIENDLYNAME, out RegType, ptrBuf,
                        BUFFER_SIZE, out RequiredSize))
                    {
                        const int utf16terminatorSize_bytes = 2;
                        string friendlyName = System.Text.UnicodeEncoding.Unicode.GetString(ptrBuf, 0, (int)RequiredSize - utf16terminatorSize_bytes);

                        if (!friendlyName.StartsWith(friendlyNamePrefix))
                            continue;

                        if (!friendlyNameToComPort.IsMatch(friendlyName))
                            continue;

                        return friendlyNameToComPort.Match(friendlyName).Groups[1].Value;
                    }
                } // devices
                SetupDiDestroyDeviceInfoList(h);
            }
        } // class guids

        return null;
    }

    /// <summary>
    /// The SP_DEVINFO_DATA structure defines a device instance that is a member of a device information set.
    /// </summary>
    [StructLayout(LayoutKind.Sequential)]
    private struct SP_DEVINFO_DATA
    {
        /// <summary>Size of the structure, in bytes.</summary>
        public uint cbSize;
        /// <summary>GUID of the device interface class.</summary>
        public Guid ClassGuid;
        /// <summary>Handle to this device instance.</summary>
        public uint DevInst;
        /// <summary>Reserved; do not use.</summary>
        public uint Reserved;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct SP_DEVICE_INTERFACE_DATA
    {
        public Int32 cbSize;
        public Guid interfaceClassGuid;
        public Int32 flags;
        private UIntPtr reserved;
    }

    const int BUFFER_SIZE = 1024;

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
    private struct SP_DEVICE_INTERFACE_DETAIL_DATA
    {
        public int cbSize;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = BUFFER_SIZE)]
        public string DevicePath;
    }

    private enum SPDRP
    {
        DEVICEDESC = 0x00000000,
        HARDWAREID = 0x00000001,
        COMPATIBLEIDS = 0x00000002,
        NTDEVICEPATHS = 0x00000003,
        SERVICE = 0x00000004,
        CONFIGURATION = 0x00000005,
        CONFIGURATIONVECTOR = 0x00000006,
        CLASS = 0x00000007,
        CLASSGUID = 0x00000008,
        DRIVER = 0x00000009,
        CONFIGFLAGS = 0x0000000A,
        MFG = 0x0000000B,
        FRIENDLYNAME = 0x0000000C,
        LOCATION_INFORMATION = 0x0000000D,
        PHYSICAL_DEVICE_OBJECT_NAME = 0x0000000E,
        CAPABILITIES = 0x0000000F,
        UI_NUMBER = 0x00000010,
        UPPERFILTERS = 0x00000011,
        LOWERFILTERS = 0x00000012,
        MAXIMUM_PROPERTY = 0x00000013,
    }

    [DllImport("setupapi.dll", SetLastError = true)]
    static extern bool SetupDiClassGuidsFromName(string ClassName,
        ref Guid ClassGuidArray1stItem, UInt32 ClassGuidArraySize,
        out UInt32 RequiredSize);

    [DllImport("setupapi.dll")]
    internal static extern IntPtr SetupDiGetClassDevsEx(IntPtr ClassGuid,
        [MarshalAs(UnmanagedType.LPStr)]String enumerator,
        IntPtr hwndParent, Int32 Flags, IntPtr DeviceInfoSet,
        [MarshalAs(UnmanagedType.LPStr)]String MachineName, IntPtr Reserved);

    [DllImport("setupapi.dll")]
    internal static extern Int32 SetupDiDestroyDeviceInfoList(IntPtr DeviceInfoSet);

    [DllImport(@"setupapi.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern Boolean SetupDiEnumDeviceInterfaces(
       IntPtr hDevInfo,
       IntPtr optionalCrap, //ref SP_DEVINFO_DATA devInfo,
       ref Guid interfaceClassGuid,
       UInt32 memberIndex,
       ref SP_DEVICE_INTERFACE_DATA deviceInterfaceData
    );

    [DllImport("setupapi.dll")]
    private static extern Int32 SetupDiEnumDeviceInfo(IntPtr DeviceInfoSet,
        Int32 MemberIndex, ref SP_DEVINFO_DATA DeviceInterfaceData);

    [DllImport("setupapi.dll")]
    private static extern Int32 SetupDiClassNameFromGuid(ref Guid ClassGuid,
        StringBuilder className, Int32 ClassNameSize, ref Int32 RequiredSize);

    [DllImport("setupapi.dll")]
    private static extern Int32 SetupDiGetClassDescription(ref Guid ClassGuid,
        StringBuilder classDescription, Int32 ClassDescriptionSize, ref Int32 RequiredSize);

    [DllImport("setupapi.dll")]
    private static extern Int32 SetupDiGetDeviceInstanceId(IntPtr DeviceInfoSet,
        ref SP_DEVINFO_DATA DeviceInfoData,
        StringBuilder DeviceInstanceId, Int32 DeviceInstanceIdSize, ref Int32 RequiredSize);

    [DllImport("setupapi.dll", CharSet = CharSet.Auto)]
    static extern IntPtr SetupDiGetClassDevs(           // 1st form using a ClassGUID only, with null Enumerator
       ref Guid ClassGuid,
       IntPtr Enumerator,
       IntPtr hwndParent,
       int Flags
    );

    [DllImport(@"setupapi.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern Boolean SetupDiGetDeviceInterfaceDetail(
       IntPtr hDevInfo,
       ref SP_DEVICE_INTERFACE_DATA deviceInterfaceData,
       ref SP_DEVICE_INTERFACE_DETAIL_DATA deviceInterfaceDetailData,
       UInt32 deviceInterfaceDetailDataSize,
       out UInt32 requiredSize,
       ref SP_DEVINFO_DATA deviceInfoData
    );

    /// <summary>
    /// The SetupDiGetDeviceRegistryProperty function retrieves the specified device property.
    /// This handle is typically returned by the SetupDiGetClassDevs or SetupDiGetClassDevsEx function.
    /// </summary>
    /// <param Name="DeviceInfoSet">Handle to the device information set that contains the interface and its underlying device.</param>
    /// <param Name="DeviceInfoData">Pointer to an SP_DEVINFO_DATA structure that defines the device instance.</param>
    /// <param Name="Property">Device property to be retrieved. SEE MSDN</param>
    /// <param Name="PropertyRegDataType">Pointer to a variable that receives the registry data Type. This parameter can be NULL.</param>
    /// <param Name="PropertyBuffer">Pointer to a buffer that receives the requested device property.</param>
    /// <param Name="PropertyBufferSize">Size of the buffer, in bytes.</param>
    /// <param Name="RequiredSize">Pointer to a variable that receives the required buffer size, in bytes. This parameter can be NULL.</param>
    /// <returns>If the function succeeds, the return value is nonzero.</returns>
    [DllImport("setupapi.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern bool SetupDiGetDeviceRegistryProperty(
        IntPtr DeviceInfoSet,
        ref SP_DEVINFO_DATA DeviceInfoData,
        uint Property,
        out UInt32 PropertyRegDataType,
        byte[] PropertyBuffer,
        uint PropertyBufferSize,
        out UInt32 RequiredSize);


    const int DIGCF_DEFAULT = 0x1;
    const int DIGCF_PRESENT = 0x2;
    const int DIGCF_ALLCLASSES = 0x4;
    const int DIGCF_PROFILE = 0x8;
    const int DIGCF_DEVICEINTERFACE = 0x10;
    const int INVALID_HANDLE_VALUE = -1;

    private static Guid[] GetClassGUIDs(string className)
    {
        UInt32 requiredSize = 0;
        Guid[] guidArray = new Guid[1];

        bool status = SetupDiClassGuidsFromName(className, ref guidArray[0], 1, out requiredSize);
        if (true == status)
        {
            if (1 < requiredSize)
            {
                guidArray = new Guid[requiredSize];
                SetupDiClassGuidsFromName(className, ref guidArray[0], requiredSize, out requiredSize);
            }
        }
        else
            throw new System.ComponentModel.Win32Exception();

        return guidArray;
    }


}

【讨论】:

  • 我只是想回来说我一直在使用这段代码中的废话。最近有很多使用串行设备的项目。谢谢!
  • 谢谢你,有点晚了。
  • 前缀 = 名称字符串的开头。
  • 这是最好的圣诞礼物。永远!
  • 要使此代码在 64 位系统上运行,您需要将“保留”字段的类型从 uint 更改为 IntPtr(8 字节而不是 4在 64 位上)。请注意,它是一个指针,而不是 ulong 本身!参考msdn.microsoft.com/en-us/library/windows/hardware/…
【解决方案2】:

This article 的代码为我完成了这项工作(他链接到这篇文章,但他自己似乎没有在这里提供答案)。这是作者的代码:

using System.Management;
internal class ProcessConnection { 

   public static ConnectionOptions ProcessConnectionOptions()
   {
     ConnectionOptions options = new ConnectionOptions();
     options.Impersonation = ImpersonationLevel.Impersonate;
     options.Authentication = AuthenticationLevel.Default;
     options.EnablePrivileges = true;
     return options;
   }

   public static ManagementScope ConnectionScope(string machineName, ConnectionOptions options, string path)
   {
     ManagementScope connectScope = new ManagementScope();
     connectScope.Path = new ManagementPath(@"\\" + machineName + path);
     connectScope.Options = options;
     connectScope.Connect();
     return connectScope;
   }
}

public class COMPortInfo
{
   public string Name { get; set; }
   public string Description { get; set; }

   public COMPortInfo() { }     

   public static List<COMPortInfo> GetCOMPortsInfo()
   {
     List<COMPortInfo> comPortInfoList = new List<COMPortInfo>();

     ConnectionOptions options = ProcessConnection.ProcessConnectionOptions();
     ManagementScope connectionScope = ProcessConnection.ConnectionScope(Environment.MachineName, options, @"\root\CIMV2");

     ObjectQuery objectQuery = new ObjectQuery("SELECT * FROM Win32_PnPEntity WHERE ConfigManagerErrorCode = 0");
     ManagementObjectSearcher comPortSearcher = new ManagementObjectSearcher(connectionScope, objectQuery);

     using (comPortSearcher)
     {
       string caption = null;
       foreach (ManagementObject obj in comPortSearcher.Get())
       {
         if (obj != null)
         {
           object captionObj = obj["Caption"];
           if (captionObj != null)
           {
              caption = captionObj.ToString();
              if (caption.Contains("(COM"))
              {
                COMPortInfo comPortInfo = new COMPortInfo();
                comPortInfo.Name = caption.Substring(caption.LastIndexOf("(COM")).Replace("(", string.Empty).Replace(")",
                                                     string.Empty);
                comPortInfo.Description = caption;
                comPortInfoList.Add(comPortInfo);
              }
           }
         }
       }
     } 
     return comPortInfoList;
   } 
}

用法:

foreach (COMPortInfo comPort in COMPortInfo.GetCOMPortsInfo())
{
  Console.WriteLine(string.Format("{0} – {1}", comPort.Name, comPort.Description));
}

【讨论】:

    【解决方案3】:

    我知道这是用 C# 发布的,但我确信这很容易转换...

        Public Function foo() As Integer
        Try
            Dim searcher As New ManagementObjectSearcher( _
                "root\CIMV2", _
                "SELECT * FROM Win32_SerialPort")
    
            For Each queryObj As ManagementObject In searcher.Get()
                Debug.WriteLine(queryObj("Caption"))
                Debug.WriteLine(queryObj("Description"))
                Debug.WriteLine(queryObj("DeviceID"))
                Debug.WriteLine(queryObj("Name"))
                Debug.WriteLine(queryObj("PNPDeviceID"))
    
            Next
        Catch err As ManagementException
            Stop
        End Try
    End Function
    
    Public Function bar() As Integer
        Try
            Dim searcher As New ManagementObjectSearcher( _
                "root\CIMV2", _
                "SELECT * FROM Win32_PnPEntity WHERE ConfigManagerErrorCode = 0")
    
            For Each queryObj As ManagementObject In searcher.Get()
                If queryObj("Caption").ToString.Contains("(COM") Then
                    Debug.WriteLine(queryObj("Caption"))
                    Debug.WriteLine(queryObj("Description"))
                    Debug.WriteLine(queryObj("DeviceID"))
                    Debug.WriteLine(queryObj("Name"))
                    Debug.WriteLine(queryObj("PNPDeviceID"))
                End If
            Next
        Catch err As ManagementException
            Stop
        End Try
    End Function
    

    它会找到我所有的 com 端口、调制解调器、串口、usb 和蓝牙。

    【讨论】:

      【解决方案4】:

      Pavel 的课程 SetupDiWrap 效果很好,它只需要针对 Windows 7 进行一些小调整。

      希望此更新将帮助其他人(如我)在 Windows 7 中从 VCP 名称获取 COM 端口号。

      1) SP_DEVINFO_DATA 在 Windows 7 中发生了变化,总长度不再是 28 字节,而是 32 字节长。这对我有用:

         private struct SP_DEVINFO_DATA
              {
                  /// <summary>Size of the structure, in bytes.</summary>
                  public int cbSize;
                  /// <summary>GUID of the device interface class.</summary>
                  public Guid ClassGuid;
                  /// <summary>Handle to this device instance.</summary>
                  public int DevInst;
                  /// <summary>Reserved; do not use.</summary>
                  public ulong Reserved;
              }
      

      注意 ulong 表示保留而不是 int。将 uint cbSize 更改为 int cbSize 为我节省了稍后的演员阵容,否则您可以将其保留为 uint。

      2) 我也写了一行:

       da.cbSize = (uint)Marshal.SizeOf(da);
      

      为了清楚起见,将 cbSize 设置为 32 位有点不同:

      da.cbSize = Marshal.SizeOf(typeof(SP_DEVINFO_DATA));
      

      3) 我变了

       [DllImport("setupapi.dll", CharSet = CharSet.Auto)]
              static extern IntPtr SetupDiGetClassDevs(       
                 ref Guid ClassGuid,
                 IntPtr Enumerator,
                 IntPtr hwndParent,
                 int Flags
              );
      

       [DllImport("setupapi.dll", CharSet = CharSet.Auto)]
              private static extern IntPtr SetupDiGetClassDevs( 
                 ref Guid ClassGuid,
                 UInt32 Enumerator,
                 IntPtr hwndParent,
                 UInt32 Flags
              );
      

      枚举器不再是IntPtr,因此您需要像这样调用SetupDiGetClassDevs

      IntPtr h = SetupDiGetClassDevs(ref guidClone, 0, IntPtr.Zero, DIGCF_PRESENT | DIGCF_PROFILE);
      

      传递枚举器时注意“0”而不是IntPtr.Zero

      代码现在在 Windows 7 中运行起来就像一个魅力!

      【讨论】:

      • 你为什么不编辑我的答案?让我们让人们去一个地方。
      【解决方案5】:

      尝试在 Win32_SerialPort 类上运行 WMI 查询。下载 WmiCodeCreator 进行实验并自动生成 C# 代码。

      【讨论】:

      • 我不知道为什么,但是我在使用 WMI 的系统上找不到除了 COM1 之外的任何东西。即使指定像Select * from Win32_SerialPort where Name = 'COM11' 这样的已知端口也没有返回任何结果。不过,setupapi 解决方案运行良好。知道为什么 WMI 只知道 COM1 吗?
      • @Tim - 以前从未听说过这种失败,不知道。
      • @Hans Passant:根据link,使用 WMI 查询“SELECT * FROM Win32_SerialPort”并不总是返回所有 COM 端口。该链接提供了另一种解决方案。
      • @markyd13 遗憾的是,该链接不再有效。你还记得替代解决方案是什么吗?
      • @Pithikos 你可以在这里找到链接creativecodedesign.com/2010/03/14/…
      【解决方案6】:

      您可能还想考虑使用注册表,因为我发现 WMI 很慢(5 或 6 秒)

      在我的例子中,我想用已知的友好名称识别设备的 COM 端口名称。使用 regedit 我在注册表中搜索了一个包含 COM 端口的键中的友好名称。因为我找到的密钥是某种随机 ID 以及其他 10 个 ID,所以我上了几级以找到适合在其中搜索的密钥。

      我想出的代码如下:

      Dim searchFriendlyName = "Your Device Name".ToLower
      Dim k0 = Registry.LocalMachine.OpenSubKey("SYSTEM\CurrentControlSet\Enum\USB\", False)
      For Each k1Name In k0.GetSubKeyNames
          Dim k1 = k0.OpenSubKey(k1Name, False)
          For Each k2name In k1.GetSubKeyNames
              Dim k2 = k1.OpenSubKey(k2name, False)
              If k2.GetValueNames.Contains("FriendlyName") AndAlso k2.GetValue("FriendlyName").ToString.ToLower.Contains(searchFriendlyName) Then
                  If k2.GetSubKeyNames.Contains("Device Parameters") Then
                      Dim k3 = k2.OpenSubKey("Device Parameters", False)
                      If k3.GetValueNames.Contains("PortName") Then
                          For Each s In SerialPort.GetPortNames
                              If k3.GetValue("PortName").ToString.ToLower = s.ToLower Then
                                  Return s
                              End If
                          Next
                      End If
                  End If
              End If
          Next
      Next
      

      这当然需要根据您的设备在注册表中的显示方式和位置进行修改,但如果您尝试“自动检测”特定类型设备的 Com 端口,那么您应该能够做到这一点工作。

      请记住,如果您必须递归搜索大量键,那么这会减慢上述解决方案的速度,因此请尝试在注册表中找到正确的位置进行查找。

      我还在注册表搜索后添加了 WMI 代码,以防注册表搜索为空:

      Dim mg As New System.Management.ManagementClass("Win32_SerialPort")
      Try
      
          For Each i In mg.GetInstances()
              Dim name = i.GetPropertyValue("Name")
              If name IsNot Nothing AndAlso name.ToString.ToLower.Contains(searchFriendlyName.ToLower) Then
                  Return i.GetPropertyValue("DeviceId").ToString
              End If
          Next
      Finally
          mg.Dispose()
      End Try
      

      【讨论】:

        【解决方案7】:

        如果您专门使用 USB 设备而不是其他类型的 COM 端口,则可以运行跨平台 USB 库 libusbp 以显示如何根据 USB 产品查找 COM 端口名称COM 端口的 ID 和供应商 ID。

        与尝试在设备管理器中使用友好名称相比,这是一个不同但可能更好的选择。如果你真的想要的话,也许可以扩展 libusbp 以允许访问友好名称。

        【讨论】:

          【解决方案8】:

          我使用com0com 生成的虚拟端口。 有了它,您就可以根据需要命名端口 - 所以我有如下端口标题: "com0com - bus for serial port pair emulator 0 (COMA &lt;-&gt; COMB)"

          使用上面发布的 WMI 代码,您将获得该端口的 名称 "COMA &lt;-&gt; COMB"。 好像这样的构造是没有端口的,但是上面的代码会把它当成串口...

          顺便说一句,"COMA" 是一个完全有效的端口名称...(这意味着,仅查找末尾的数字是不够的)。

          所以我想知道如何可靠地区分有效的现有串行端口名称和这些奇怪的结构......

          【讨论】:

          • 为什么要设置“答案没用”?
          • Mich0815 - 我怀疑是因为这只是为您提供了另一个假端口和名称以中继到未知端口。他仍然会遇到需要自动发现实际设备端口以首先设置该映射的问题。
          猜你喜欢
          • 1970-01-01
          • 2021-10-18
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多