How to get friendly names for COM ports in C#




Recently I was developing a little application that needed to enumerate existing COM ports on a machine and then display them in a menu. Generally, this task is quite simple to accomplish:

System.IO.Ports.SerialPort.GetPortNames();

I believe under the hood, this method simply enumerated the HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP\SERIALCOMM key.

However, the tricky part was that I wanted to display human-friendly names that identify the device similar to they way they are displayed in the Device Manager. Well, this turned out to be a little more complicated than I thought.

Google revealed a couple of strategies that turned out to be limited. 

1. WMI

There are a couple of WMI objects that one could enumerate to get a list of ports. However, none seem to produce a complete set.

SELECT * FROM Win32_SerialPort
or
SELECT * FROM Win32_PnPEntity

With the first query you can extract the DeviceID property, and if it looks like COMX (where X is a number), you can get the "Name" property which should contain the friendly port name. Unfortunately, this method does not actually return all COM port devices. (i.e. SerialPort.GetPortNames() returns 4 COM ports, but the WMI select only returns 2..)

The second query enumerates all PnP devices but it has even more limitations. There is no property that identifies the port and the crappy solution is to hope that the device name contains a reference to port (i.e. "Communication Port (COM1)"). So basically, fail.

 

2. SETUPAPI.DLL

This library lives in C:\Windows\system32 and exports several methods for enumerating hardware devices. You can lookup the exported methods using Depends and PInvoke them. How to do this is outside the scope of this post, but there is info out on the web.

There is also a C++ library (EnumSerialPorts) out there that encapsulates some of these methods above (and has a couple more), but similarly does not get all the ports in any of its implementation wrappers.

 

So after hitting all these road blocks, I decided to go brute force, and fish the info out of the registry. The idea is somewhat plain and non-elegant - Get a list of non-friendly ports from GetPortNames(), Enumerate the CurrentControlSet\Enum and all of it's subkeys, if a key has a subkey "Device Parameters", check if it has a "PortName" value and if it matches any of the non-friendly names. If this is true, get the "FriendlyName" value from the parent key.. Phew.. 

UPDATE 2011.09.20 - I updated the code to incorporate suggestions below and switched to use generic collections. Also, the return value was changed to use the non-friendly name as key and the friendly name as the value of the dictionary.

The code is below and basically you call it like this:

Dictionary<string, string> friendlyPorts = BuildPortNameHash(SerialPort.GetPortNames());

 

static void Main(string[] args)
{
    Dictionary<string, string> friendlyPorts = BuildPortNameHash(SerialPort.GetPortNames());
    foreach (KeyValuePair<string, string> kvp in friendlyPorts)
    {
        Console.WriteLine("Port '{0}' is better known as '{1}'", kvp.Key, kvp.Value);
    }
}
/// <summary>
/// Begins recursive registry enumeration
/// </summary>
/// <param name="portsToMap">array of port names (i.e. COM1, COM2, etc)</param>
/// <returns>a hashtable mapping Friendly names to non-friendly port values</returns>
static Dictionary<string, string> BuildPortNameHash(string[] portsToMap)
{
    Dictionary<string, string> oReturnTable = new Dictionary<string, string>();
    MineRegistryForPortName("SYSTEM\\CurrentControlSet\\Enum", oReturnTable, portsToMap);
    return oReturnTable;
}
/// <summary>
/// Recursively enumerates registry subkeys starting with startKeyPath looking for 
/// "Device Parameters" subkey. If key is present, friendly port name is extracted.
/// </summary>
/// <param name="startKeyPath">the start key from which to begin the enumeration</param>
/// <param name="targetMap">dictionary that will get populated with 
/// nonfriendly-to-friendly port names</param>
/// <param name="portsToMap">array of port names (i.e. COM1, COM2, etc)</param>
static void MineRegistryForPortName(string startKeyPath, Dictionary<string, string> targetMap, 
    string[] portsToMap)
{
    if (targetMap.Count >= portsToMap.Length)
        return;
    using (RegistryKey currentKey = Registry.LocalMachine)
    {
        try
        {
            using (RegistryKey currentSubKey = currentKey.OpenSubKey(startKeyPath))
            {
                string[] currentSubkeys = currentSubKey.GetSubKeyNames();
                if (currentSubkeys.Contains("Device Parameters") && 
                    startKeyPath != "SYSTEM\\CurrentControlSet\\Enum")
                {
                    object portName = Registry.GetValue("HKEY_LOCAL_MACHINE\\" +
                        startKeyPath + "\\Device Parameters", "PortName", null);
                    if (portName == null ||
                        portsToMap.Contains(portName.ToString()) == false)
                        return;
                    object friendlyPortName = Registry.GetValue("HKEY_LOCAL_MACHINE\\" +
                        startKeyPath, "FriendlyName", null);
                    string friendlyName = "N/A";
                    if (friendlyPortName != null)
                        friendlyName = friendlyPortName.ToString();
                    if (friendlyName.Contains(portName.ToString()) == false)
                        friendlyName = string.Format("{0} ({1})", friendlyName, portName);
                    targetMap[portName.ToString()] = friendlyName;
                }
                else
                {
                    foreach (string strSubKey in currentSubkeys)
                        MineRegistryForPortName(startKeyPath + "\\" + strSubKey, targetMap, portsToMap);
                }
            }
        }
        catch (Exception)
        {
            Console.WriteLine("Error accessing key '{0}'.. Skipping..", startKeyPath);
        }
    }
}

One thing to note, is that occasionally, even running as Administrator, you won't have access to certain keys. I ran into this with ACPI\FixedButton\Properties key. Thus, it would me a good idea to wrap the 'oCurrentKey = oCurrentKey.OpenSubKey(strStartKey);' in a try/catch, and just return empty if an exception occurs.

Let me know your thoughts,

Cheers!

 

 



I am running .NET 2.0 and had to make a small revision to the lines :
' portsToMap.Contains(portName.ToString()) == false)'
and

'if (friendlyName.Contains(portName.ToString()) == false)'

Since .Contains is not available to the System.Array class I modified it to:

'String.Join(sep,portsToMap).Contains(portName.ToString()) == false)'

This will resolve any compiler issues.

Otherwise great work. Thanks

DB



Thanks!
I tryed WMI but all crashs in Win7 (PortNames), also tryed all kind of stuf, lets see if the registry is right and detects my usb to serial converter and its accesible from W7.



it works for me, on window 7 thank you .



According to my "C# 4.0 How-To" book,
"Registry keys are represented by handles, which are system resources that must be disposed of, hence the 'using' statements."
So I altered the code to:
using (RegistryKey oCurrentKey = Registry.LocalMachine)
  try
  {
    using (RegistryKey oCurrentKey2 = oCurrentKey.OpenSubKey(strStartKey))
    {
      string[] strSubKeyNames = oCurrentKey2.GetSubKeyNames();
    .
    .
    .
  catch (SecurityException)
  {
    Console.WriteLine("Oops");
  }

The catch statement takes care of occasional registry key access errors (those aren't the keys we're interested in anyway, so just skip them).



I've been meaning to revisit the code to change a couple of things around, and I integrated your suggestions above.

Cheers!



hi,
ur code is so good
but there is a problem with permissions,
plz explain how to solve this problem?
"Requested registry access is not allowed."

i wrapped it with try catch block,
but there is another problem
"Object reference not set to an instance of an object."
on this part of code
" string[] oSubKeyNames = oCurrentKey.GetSubKeyNames();"



Merci!

You saved my afternoon.
The registry keys seems to be the same on XP (32 bits) and win7(64 bits).



I have been searching for ages and tried so many different things (way more complex) to be able to achieve this.

Thank you so much.



Your code sample has been REALLY helpful.

I think that learning to deal with the registry is vital for a programmer (please note: I'm a newbie). The problem is, as always, how to learn.

Thanks a lot!
Andrea



Ads



User login