How do I get a list of available serial ports in Win32?
Asked Answered
S

6

65

I have some legacy code that provides a list of the available COM ports on the PC by calling the EnumPorts() function and then filtering for the port names that start with "COM".

For testing purposes it would be very useful if I could use this code with something like com0com, which provides pairs of virtual COM ports looped together as a null-modem.

However the com0com ports are not found by the EnumPorts() function (even without filtering for "COM"). HyperTerminal and SysInternals PortMon can both see them, so I'm sure it is installed correctly.

So is there some other Win32 function that provides a definitive list of available serial ports?

Sollars answered 7/9, 2009 at 11:19 Comment(0)
S
99

The EnumSerialPorts v1.20 suggested by Nick D uses nine different methods to list the serial ports! We're certainly not short on choice, though the results seem to vary.

To save others the trouble, I'll list them here and indicate their success in finding the com0com ports on my PC (XP Pro SP2):

  1. CreateFile("COM" + 1->255) as suggested by Wael Dalloul
    ✔ Found com0com ports, took 234ms.

  2. QueryDosDevice()
    ✔ Found com0com ports, took 0ms.

  3. GetDefaultCommConfig("COM" + 1->255)
    ✔ Found com0com ports, took 235ms.

  4. "SetupAPI1" using calls to SETUPAPI.DLL
    ✔ Found com0com ports, also reported "friendly names", took 15ms.

  5. "SetupAPI2" using calls to SETUPAPI.DLL
    ✘ Did not find com0com ports, reported "friendly names", took 32ms.

  6. EnumPorts()
    ✘ Reported some non-COM ports, did not find com0com ports, took 15ms.

  7. Using WMI calls
    ✔ Found com0com ports, also reported "friendly names", took 47ms.

  8. COM Database using calls to MSPORTS.DLL
    ✔/✘ Reported some non-COM ports, found com0com ports, took 16ms.

  9. Iterate over registry key HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP\SERIALCOMM
    ✔ Found com0com ports, took 0ms. This is apparently what SysInternals PortMon uses.

Based on those results I think the WMI method probably suits my requirements best as it is relatively fast and as a bonus it also gives the friendly names (e.g. "Communications Port (COM1)", "com0com - serial port emulator").

Sollars answered 8/9, 2009 at 14:20 Comment(9)
@GrahamS: great answer - QueryDosDevice() worked really well when searching for FTDI USB<->Serial port devices where other methods had failed.Elinaelinor
Glad it helped Jon, though @Nick Dandoulakis really deserves most of the credit for pointing me to EnumSerialPorts in the first place.Sollars
You have to prepend \\.\ to access COM > 9 because they are not reserved in NT namespace and are only accessible in the device namespace -- learn.microsoft.com/ru-ru/windows/desktop/FileIO/…Remillard
Correct @aitap, already mentioned in the other answers but bears repeating. Thanks.Sollars
@GrahamS, I only commented because I've tried to edit the answer, but the "save" button always times out for me.Remillard
It's not so black and white with these timings: I had a case with customer laptops which had Bluetooth serial ports and the WMI query of serial ports took several minutes. I guess it tried to talk to Bluetooth devices. We eventually had to sacrifice the user friendly port names.Salisbarry
Good to know, thanks @MikkL. Did you try any of the other methods with friendly names? What one worked best for you in the end?Sollars
@Sollars - we used the C# .NET SerialPort.GetPortNames in the end, which gives plain port names COM1, COM2, etc. Don't know which method does .NET use under the hood, but it performed fast. I can't comment how would other friendly name getting methods perform with Bluetooth ports.Salisbarry
@MikkL. Apparently, .NET uses method 9 (Iterate over registry key HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP\SERIALCOMM): referencesource.microsoft.com/#System/sys/system/io/ports/…Acrylyl
B
14

It appears that it's not a simple task.

Check out this: EnumSerialPorts v1.20

Baroda answered 7/9, 2009 at 11:43 Comment(2)
Thanks Nick, I've expanded a little on your answer below. If no one comes along with anything more definite then I'll accept your response.Sollars
Beware: "If you want to distribute source code with your application, then you are only allowed to distribute versions released by the author." This makes it useless for anything open source--you can't even fix bugs unless the original author does it for you. I'd suggest anyone looking for serial code to find something under a less closed-minded license.Elke
O
6

you can make loop for example from 1 to 50 and try to open each port. If the port is available, the open will work. If the port is in use, you'll get a sharing error. If the port is not installed, you'll get a file not found error.

to open the port use CreateFile API:

HANDLE Port = CreateFile(
                  "\\\\.\\COM1",
                  GENERIC_READ | GENERIC_WRITE,
                  0,
                  NULL,
                  OPEN_EXISTING,
                  FILE_ATTRIBUTE_NORMAL,
                  NULL);

then check the result.

Oriya answered 7/9, 2009 at 11:42 Comment(1)
It should be mentioned that if you try to access COM ports > 9 with CreateFile, you'll always get ERROR_FILE_NOT_FOUND, even if the port exists. To avoid that behaviour, the port name should be passed as \\.\COMx (replacing x with the port number we want to test). Link: support.microsoft.com/kb/115831Farina
J
4

It's available now in Windows, GetCommPorts can directly return a list of comm ports

Gets an array that contains the well-formed COM ports.

This function obtains the COM port numbers from the HKLM\Hardware\DeviceMap\SERIALCOMM registry key and then writes them to a caller-supplied array. If the array is too small, the function gets the necessary size.

you will need to add this code in order to link the function correctly

#pragma comment (lib, "OneCore.lib")
Joris answered 15/9, 2022 at 10:18 Comment(1)
Thanks. That function seems to iterate over HKLM\Hardware\DeviceMap\SERIALCOMM for you, (previously mentioned) and it only gives back an array of port numbers, not the useful text names/descriptions that you can get when you do the iteration yourself.Sollars
F
3

In my case, I need both the full names and COM port addresses. I have physical serial ports, USB serial ports, and com0com virtual serial ports.

Like the accepted answer suggests, I use WMI calls. SELECT * FROM Win32_PnPEntity find all devices. It returns physical devices like this, and address can be parsed from Caption:

Serial Port for Barcode Scanner (COM13)

However, for com0com ports Caption is like this (no address):

com0com - serial port emulator

SELECT * FROM Win32_SerialPort returns addresses (DeviceID), as well as full names (Name). However, it only finds physical serial ports and com0com ports, not USB serial ports.

So in the end, I need two WMI calls: SELECT * FROM Win32_SerialPort (address is DeviceID) and SELECT * FROM Win32_PnPEntity WHERE Name LIKE '%(COM%' (address can be parsed from Caption). I have narrowed down the Win32_PnPEntity call, because it only needs to find devices that were not found in the first call.

This C++ code can be used to find all serial ports:

// Return list of serial ports as (number, name)
std::map<int, std::wstring> enumerateSerialPorts()
{
    std::map<int, std::wstring> result;

    HRESULT hres;

    hres = CoInitializeEx(0, COINIT_APARTMENTTHREADED);
    if (SUCCEEDED(hres) || hres == RPC_E_CHANGED_MODE) {
        hres =  CoInitializeSecurity(
            NULL,
            -1,                          // COM authentication
            NULL,                        // Authentication services
            NULL,                        // Reserved
            RPC_C_AUTHN_LEVEL_DEFAULT,   // Default authentication
            RPC_C_IMP_LEVEL_IMPERSONATE, // Default Impersonation
            NULL,                        // Authentication info
            EOAC_NONE,                   // Additional capabilities
            NULL                         // Reserved
            );

        if (SUCCEEDED(hres) || hres == RPC_E_TOO_LATE) {
            IWbemLocator *pLoc = NULL;

            hres = CoCreateInstance(
                CLSID_WbemLocator,
                0,
                CLSCTX_INPROC_SERVER,
                IID_IWbemLocator, (LPVOID *) &pLoc);

            if (SUCCEEDED(hres)) {
                IWbemServices *pSvc = NULL;

                // Connect to the root\cimv2 namespace with
                // the current user and obtain pointer pSvc
                // to make IWbemServices calls.
                hres = pLoc->ConnectServer(
                     bstr_t(L"ROOT\\CIMV2"),  // Object path of WMI namespace
                     NULL,                    // User name. NULL = current user
                     NULL,                    // User password. NULL = current
                     0,                       // Locale. NULL indicates current
                     NULL,                    // Security flags.
                     0,                       // Authority (for example, Kerberos)
                     0,                       // Context object
                     &pSvc                    // pointer to IWbemServices proxy
                     );
                if (SUCCEEDED(hres)) {
                    hres = CoSetProxyBlanket(
                       pSvc,                        // Indicates the proxy to set
                       RPC_C_AUTHN_WINNT,           // RPC_C_AUTHN_xxx
                       RPC_C_AUTHZ_NONE,            // RPC_C_AUTHZ_xxx
                       NULL,                        // Server principal name
                       RPC_C_AUTHN_LEVEL_CALL,      // RPC_C_AUTHN_LEVEL_xxx
                       RPC_C_IMP_LEVEL_IMPERSONATE, // RPC_C_IMP_LEVEL_xxx
                       NULL,                        // client identity
                       EOAC_NONE                    // proxy capabilities
                    );
                    if (SUCCEEDED(hres)) {
                        // Use Win32_PnPEntity to find actual serial ports and USB-SerialPort devices
                        // This is done first, because it also finds some com0com devices, but names are worse
                        IEnumWbemClassObject* pEnumerator = NULL;
                        hres = pSvc->ExecQuery(
                            bstr_t(L"WQL"),
                            bstr_t(L"SELECT Name FROM Win32_PnPEntity WHERE Name LIKE '%(COM%'"),
                            WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY,
                            NULL,
                            &pEnumerator);

                        if (SUCCEEDED(hres)) {
                            constexpr size_t max_ports = 30;
                            IWbemClassObject *pclsObj[max_ports] = {};
                            ULONG uReturn = 0;

                            do {
                                hres = pEnumerator->Next(WBEM_INFINITE, max_ports, pclsObj, &uReturn);
                                if (SUCCEEDED(hres)) {
                                    for (ULONG jj = 0; jj < uReturn; jj++) {
                                        VARIANT vtProp;
                                        pclsObj[jj]->Get(L"Name", 0, &vtProp, 0, 0);

                                        // Name should be for example "Serial Port for Barcode Scanner (COM13)"
                                        const std::wstring deviceName = vtProp.bstrVal;
                                        const std::wstring prefix = L"(COM";
                                        size_t ind = deviceName.find(prefix);
                                        if (ind != std::wstring::npos) {
                                            std::wstring nbr;
                                            for (size_t i = ind + prefix.length();
                                                i < deviceName.length() && isdigit(deviceName[i]); i++)
                                            {
                                                nbr += deviceName[i];
                                            }
                                            try {
                                                const int portNumber = boost::lexical_cast<int>(nbr);
                                                result[portNumber] = deviceName;
                                            }
                                            catch (...) {}
                                        }
                                        VariantClear(&vtProp);

                                        pclsObj[jj]->Release();
                                    }
                                }
                            } while (hres == WBEM_S_NO_ERROR);
                            pEnumerator->Release();
                        }

                        // Use Win32_SerialPort to find physical ports and com0com virtual ports
                        // This is more reliable, because address doesn't have to be parsed from the name
                        pEnumerator = NULL;
                        hres = pSvc->ExecQuery(
                            bstr_t(L"WQL"),
                            bstr_t(L"SELECT DeviceID, Name FROM Win32_SerialPort"),
                            WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY,
                            NULL,
                            &pEnumerator);

                        if (SUCCEEDED(hres)) {
                            constexpr size_t max_ports = 30;
                            IWbemClassObject *pclsObj[max_ports] = {};
                            ULONG uReturn = 0;

                            do {
                                hres = pEnumerator->Next(WBEM_INFINITE, max_ports, pclsObj, &uReturn);
                                if (SUCCEEDED(hres)) {
                                    for (ULONG jj = 0; jj < uReturn; jj++) {
                                        VARIANT vtProp1, vtProp2;
                                        pclsObj[jj]->Get(L"DeviceID", 0, &vtProp1, 0, 0);
                                        pclsObj[jj]->Get(L"Name", 0, &vtProp2, 0, 0);

                                        const std::wstring deviceID = vtProp1.bstrVal;
                                        if (deviceID.substr(0, 3) == L"COM") {
                                            const int portNumber = boost::lexical_cast<int>(deviceID.substr(3));
                                            const std::wstring deviceName = vtProp2.bstrVal;
                                            result[portNumber] = deviceName;
                                        }
                                        VariantClear(&vtProp1);
                                        VariantClear(&vtProp2);

                                        pclsObj[jj]->Release();
                                    }
                                }
                            } while (hres == WBEM_S_NO_ERROR);
                            pEnumerator->Release();
                        }
                    }
                    pSvc->Release();
                }
                pLoc->Release();
            }
        }
        CoUninitialize();
    }
    if (FAILED(hres)) {
        std::stringstream ss;
        ss << "Enumerating serial ports failed. Error code: " << int(hres);
        throw std::runtime_error(ss.str());
    }

    return result;
}
Ferdy answered 10/11, 2017 at 11:38 Comment(0)
G
1

I have reorganized PJ Naughter 's EnumSerialPorts as more portable and individual forms, that is more useful.

For better in compatibility, I use C, instead of C++.

If you need or be interested in it, please visit the post in my blogger.

Garay answered 15/7, 2015 at 18:33 Comment(2)
"For better in compatibility, I use C, instead of C++." Sorry, but that's silly. It's not 1998 and there's no reason to ever use C in Windows.Elke
@GlennMaynard "there's no reason to ever use C in Windows". Of course there is! If you have a library or application already written in C, or if you have a cross-platform application that also runs on a platform where C is the only option, etc. Plenty of other reasons as well.Cloudlet

© 2022 - 2024 — McMap. All rights reserved.