Introduction
Working in the domain of lab automation, development and use of control software frequently requires the setting up of communication channels to converse with various pieces of equipment such as a robot arm or automated cell incubator. Connections to hardware can be via USB, Firewire, CANBus, I2C and Serial ports and may use converters such as a USB to 4-way serial port box or Serial to I2C converter. Contrary to most equipment manufacturers' instructions, setting an application to converse with a piece of kit via the wrong channel with the wrong settings is quite easy, as is plugging in said equipment into the wrong port. Various laws also seem to require that such cases have connectors positioned in an area of restricted space amongst wiring that has strong spaghetti-like tendencies.
The initial lab-automation developers, as they read up on the various bits of equipment involved, usually work out what to connect where on their particular development laptop. These developers are usually focused on getting a piece of kit up and running. Neglecting to document that the communication channel settings are perhaps hard-coded or recorded in some obscure settings file, has proved to be a common occurrence. To resolve this problem, two C++ classes have been written to be used by that first developer for the benefit of later developers and ultimately users. These classes allow a display of the communications channel options in a manner conducive to getting things up and running quickly and without confusion. The classes are typically used to quickly and simply display filtered device information (and device icons) in a list control. They are a thin wrapper around some of the windows setup-API functions and have the advantage that as they are relatively thin wrapper classes, little more than the original API documentation of these methods is required for use.
As an example, the screen shot below which displays data obtained via the two C++ classes shows the user that if COM13 is selected on this particular PC, the relevant piece of equipment should be plugged into the connector labelled 'Port 3' on the 4-way USB-to-Serial converter box. This sounds like a trivial exercise, but when some piece of equipment just sits there, perhaps because a certain PC has different default values for some communications channel to those used initially, or an incorrectly wired non-standard communications lead is being used, being able to prove everything is connected properly (or not) in minutes rather than hours or days is a big advantage. The example uses serial ports but is just as applicable to other types of devices and communication channels.
Background
Research into how to list, initially just information on the serial ports available but which expanded to cover all (software) devices, led first to code which used the registry. A utility accompanying a 4-way USB to serial port box showed that other useful information could also be obtained. Looking for a means to do something similar to the utility led to the use of setupapi.h and the functions it provides such as SetupDiGetClassDevs.
Microsoft article 259695: How to enumerate hardware devices using SetupDi calls gives the general idea of how to access devices. An article Enumerating Windows device by Chuan-Liang Teng also looks at this area.
Classes
Two classes: CDevInfo
and CDeviceImageList
, were written to wrap various structures and API calls. CDeviceImageList
is used to obtain an image-list of the device icons and CDevInfo
is used to access and enumerate through the device information.
CDevInfo
wraps a HDEVINFO, a
handle to a device interface set, and a SP_DEVINFO_DATA structure which defines a device in that device interface set. Setupapi calls wrapped by CDevInfo
are:
CDeviceImageList
wraps a SP_CLASSIMAGELIST_DATA structure and calls the setupapi functions SetupDiGetClassImageList and SetupDiDestroyClassImageList so as to produce the device-class icons as displayed in Device-Manager.
These two classes together with an ATL CListViewCtrl
allow a list of devices and related information to be selected using Setup Device Registry Property (SPDRP) codes. An appropriate device class icon is displayed alongside each entry. Displaying such information is accomplished using code of the following form:
m_wndListView.Attach(GetDlgItem(IDC_LIST1));
m_wndListView.InsertColumn(0, _T("Name"), LVCFMT_LEFT, 200, 0);
m_wndListView.InsertColumn(1, _T("Friendly-Name"), LVCFMT_LEFT, 200, 0);
m_wndListView.InsertColumn(2, _T("Driver"), LVCFMT_LEFT, 200, 0);
m_wndListView.InsertColumn(3, _T("Mfg"), LVCFMT_LEFT, 200, 0);
m_wndListView.InsertColumn(4, _T("Physical Device"), LVCFMT_LEFT, 200, 0);
m_wndListView.SetImageList(m_DevImageList, 1);
CDevInfo cDevInfo(m_hWnd);
int a = 0;
while(cDevInfo.EnumDeviceInfo())
{
wchar_t szBuf[MAX_PATH] = {0};
if(cDevInfo.GetDeviceRegistryProperty(SPDRP_CLASS, (PBYTE)szBuf))
{
wchar_t szFriendlyName[MAX_PATH] = {0};
cDevInfo.GetDeviceRegistryProperty(SPDRP_FRIENDLYNAME,
(PBYTE)szFriendlyName);
wchar_t szDriver[MAX_PATH] = {0};
cDevInfo.GetDeviceRegistryProperty(SPDRP_DRIVER, (PBYTE)szDriver);
wchar_t szMfg[MAX_PATH] = {0};
cDevInfo.GetDeviceRegistryProperty(SPDRP_MFG, (PBYTE)szMfg);
wchar_t szPhysical[MAX_PATH] = {0};
cDevInfo.GetDeviceRegistryProperty
(SPDRP_PHYSICAL_DEVICE_OBJECT_NAME, (PBYTE)szPhysical);
int ImageIndex = 0;
if(cDevInfo.GetClassImageIndex(m_DevImageList, &ImageIndex))
{
wchar_t szDesc[MAX_PATH] = {0};
if(cDevInfo.GetClassDescription(szDesc))
{
ATLTRACE(szDesc);
ATLTRACE(_T("\n"));
}
m_wndListView.InsertItem(a,szDesc,ImageIndex);
m_wndListView.SetItemText(a,1,szFriendlyName);
m_wndListView.SetItemText(a,2,szDriver);
m_wndListView.SetItemText(a,3,szMfg);
m_wndListView.SetItemText(a,4,szPhysical);
}
}
}
In order to be able to filter which devices were displayed, an alternate CDevInfo
constructor was added to which exposes the API use of GUIDs to specify devices of interest. An object can now be created passing GUIDs for the device type and GUIDs for any flags which are required. In the case below, the flags ensure that only Serial devices and USB-Serial devices that are actually connected to the PC are displayed:
const GUID Guid = GUID_DEVCLASS_PORTS;
CDevInfo cDevInfo(m_hWnd, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE , &Guid);
while(cDevInfo.EnumDeviceInfo())
{
wchar_t szFriendlyName[MAX_PATH] = {0};
cDevInfo.GetDeviceRegistryProperty(SPDRP_FRIENDLYNAME, (PBYTE)szFriendlyName);
...
}
Without the DIGCF_PRESENT
flag, any installed device that used a COM port would show up; whether it was physically connected to the PC or not; but when the flag DIGCF_PRESENT
is used, only USB to Serial devices that are actually plugged into the PC are displayed. As the manufacturer of the converter box being used had included the number of the connector on the box in the 'information' field, the user can now be shown that by selecting, say, COM11 for some serial device, that the serial lead from that device needs to be plugged into the connector labelled Port 1 on the converter box.
Points of Interest
When passing the image list wrapped by the CDeviceImageList
to one of the CListViewCtrl
methods, the question of image-list ownership arose. To resolve this, a non-standard copy constructor and CDeviceImageList & operator= (CDeviceImageList & pSource)
were written to pass ownership of a list in the manner of an auto-ptr rather than copy it. The CDevInfo
class was also treated in the same way so that the wrapped HDEVINFO
is passed rather than copied. The Visual Studio Project DevInfoTester
exercises this feature by creating a few CDevInfo
objects and assigning them around as shown below:
CDevInfo foo(CDevInfo & _DevInfo)
{
return _DevInfo;
}
int _tmain(int , _TCHAR* )
{
CDevInfo DevInfo1(NULL);
CDevInfo DevInfo2 = DevInfo1;
CDevInfo DevInfo3(NULL);
DevInfo3 = foo(DevInfo2);
DevInfo1 = DevInfo3;
...
Some non-default sorting of the lines in the CListViewCtrl
had to be added to display COM ports in a sensible order as without this ports were displayed in the order COM1, COM11, COM2 rather than COM1, COM2, COM11. This is demonstrated in the Visual Studio Project PortInfo
.
Use of the setupapi via CDeviceImageList
and CListViewCtrl
objects allowed 40 lines of easily maintainable code in, say, a dialog, to replace over 100 lines of less flexible code which used calls direct to the registry.
Using the Code
The accompanying VS2005 Solution includes three projects, one which, for simplicity sake, will compile without WTL and two which require WTL installed to compile (see Windows Template Library (WTL) for the download). DevInfoWin32
runs in a Command window and after exercising the CDevInfo
object displays a list of ports detected. This project is the simplest.
The ListViewTest
VS2005 project uses a dialog based project to display a list of all the devices found; sorting is not implemented in this project so as to illustrate the code required to use the classes.
The third VS2005 project, PortInfo
, uses a dialog accessed by Port | Select on the menu and displays a list of serial ports. sorting is implemented in this project.
To change the device classes displayed, use different device GUIDs. For example, in the ListViewTest
project which currently displays all devices, changing the basic CDevInfo
constructor line...
CDevInfo cDevInfo(m_hWnd);
... to...
const GUID Guid = GUID_DEVCLASS_USB;
CDevInfo cDevInfo(m_hWnd, DIGCF_PRESENT , &Guid);
... will limit the display to all USB devices.
References
The Code Project article How to use 32bits icons in CTreeCtrl showed how to load icons into a control and Another serial port enumerator is where I started. I also came across the article, Using WTL's Built-in Dialog Resizing Class, when looking for a reminder on dialog resize code. Thanks.
For Auto-pointer type operation in which resources are transferred rather than copied, see 'C++ in action 10.6-10.8, B.Milewski, ISBN 0-201-69948-6, Addison-Wesley 2001'.
History
- 12th December, 2008: Initial version