I'm writing code in C# for a Windows 10 machine. I'm using the class 'SerialPortManager.cs' from Paul van Dinther. It is using 'System.Management' and starts with scanning for connected USB Serial Port devices using WMI with the querystring:
SELECT DeviceID, PNPDeviceID FROM Win32_SerialPort
The class also provides a way to detect the connection or disconnection of USB Serial Port devices. It does this by using ManagementEventWatcher, in which 'eventType' is "__InstanceCreationEvent" (to detect connection) or "__InstanceDeletionEvent" (to detect disconnection).
private ManagementEventWatcher USBWatcherSetUp(string eventType)
{
_watcherQuery = new WqlEventQuery();
_watcherQuery.EventClassName = eventType;
_watcherQuery.WithinInterval = new TimeSpan(0, 0, 2);
_watcherQuery.Condition = @"TargetInstance ISA 'Win32_SerialPort'";
return new ManagementEventWatcher(_scope, _watcherQuery);
}
So far so good. It works great for my devices that use the PIC microcontroller (who has USB Serial Port on board), and also works great with native Arduino.
Except, I have one Arduino clone that uses the CH340 chip to connect to the USB port. I have installed the specific driver for it. It connects perfectly with my Arduino IDE, and shows as COM6.
But my code doesn't detect it.
I replaced all 'Win32_SerialPort' occurrences with 'Win32_PnPEntity', and changed the initial search query to:
SELECT DeviceID, PNPDeviceID FROM Win32_PnPEntity WHERE Name LIKE ‘%COM([0-9]%’
But no success. Then I changed it to:
SELECT DeviceID, PNPDeviceID FROM Win32_PnPEntity
Then I see that my CH340 based device is detected, and if I disconnect/reconnect I see that events are triggered. But the problem with using Win32_PnPEntity is that I don't know what the COM-Port is. And I need that COM-Port to be able to use it with the class 'SerialPort' in another part of my code.
Here some code to explain what I'm doing. Basically, when I Connect/Disconnect my device, I retrieve a ManagementBaseObject and pass it to the method 'CreatePortArgs'.
private void HandlePortAdded(object sender, EventArrivedEventArgs e)
{
var instance = e.NewEvent.GetPropertyValue("TargetInstance") as ManagementBaseObject;
SerialPortEventArgs serialPortEventArgs = CreatePortArgs(instance);
if (CheckIDMatch(serialPortEventArgs))
{
OnPortAddedEvent?.Invoke(this, serialPortEventArgs);
}
}
In the method 'CreatePortArgs', I extract the VID, PID, SerialNumber and at the end use "DeviceID" to retrieve the COM-Port:
private SerialPortEventArgs CreatePortArgs(ManagementBaseObject queryObj)
{
string PNPDeviceID = ((string)queryObj.GetPropertyValue("PNPDeviceID")).ToUpper();
int vid = 0;
int pid = 0;
string serialNumber = "";
int index = PNPDeviceID.IndexOf("VID_");
if (index > -1 && PNPDeviceID.Length >= index + 8)
{
string id = PNPDeviceID.Substring(index + 4, 4);
vid = Convert.ToInt32(id, 16);
}
index = PNPDeviceID.IndexOf("PID_");
if (index > -1 && PNPDeviceID.Length >= index + 8)
{
string id = PNPDeviceID.Substring(index + 4, 4);
pid = Convert.ToInt32(id, 16);
}
index += 9;
if (PNPDeviceID.Length > index)
serialNumber = PNPDeviceID.Substring(index);
return new SerialPortEventArgs((string)queryObj["DeviceID"], vid, pid, serialNumber, PNPDeviceID);
}
When I'm using 'Win32_SerialPort', then 'queryObj["DeviceID"]' returns the COM-Port of the found devices (this is what I see with my native Arduino or my PIC-based hardware).
When I'm using 'Win32_PnPEntity', I don't get a COM-Port. Instead I'm getting a string like "USB\VID_2341&PID_0042\8554856875F132".
I'm absolutely not a WMI expert, but I assume that the logic of this is that Win32_SerialPort is a special subset of Win32_PnPEntity, which translates its properties to COM-Ports. Using Win32_PnPEntity doesn't know about COM-Ports. Right?
Question: Is there a way to translate a PNPDeviceID in a COM-Port? Arduino IDE is also doing it, so I wonder what the trick is.
Years ago I used a complete different way that was working fine. That method was dealing with the registry, but was using some 'undocumented features', for which I'm afraid that they can break if Microsoft decides to change this. That's why I would prefer to rely on WMI, which is a fully documented methodology.
Thanks to the contribution of the people in this post, I could come up with my own solution.
First of all, the hints provided by @user246821, to have a look at the powershell commands in one of the post, helped me a lot to understand and experiment with WMI. Especially this one:
Get-CimInstance -Namespace Root\Cimv2 -Query "Select * From Win32_PnPEntity where PnPClass = 'Ports' and Name like '%COM%'"
This together with using WMICodeCreator did help me to play with WMI to find the correct way to detect all my devices, including Arduino with the CH340 chip.
The resulting code can be found in the file SerialPortManager.cs in my GitHub repository CockpitHardwareHUB_v2.
Basically, I'm using a queryString as below:
Select * From Win32_PnPEntity where PnPClass = 'Ports' and Name like '%COM%'
This is detecting all 'devices' that can be used with 'SerialPort' (at least, that is my understanding). With the above queryString, I can find my PIC-microcontrollers with onboard USB, my native Arduino and a Chinese clone Arduino using the CH340 chip.
But the PNPDeviceID doesn't contain the COM-Port, so I needed to find the correct property where the COM-Port is available. There, the above mentioned powershell command showed me that the property 'Caption' always seems to include the COM-Port. I used some Regular Expression (thanks to ChatGPT) to extract it. I ignore devices that don't have a COM-Port in the Caption property.
And that's basically it. The end result is that I can detect my device, and extract its VID, PID, SerialNumber and COM-Port. I can then use that COM-Port to open the SerialPort, and start communicating with the device.
So far, I haven't found any issues.