2022 FEB 11
In the two previous parts we looked at modifying the USB descriptor to include a custom product name and manufacturer for the SparkFun Pro Micro and Teensy 4.0. In this part we will use this information to uniquely identify our boards.
After modifying the Pro Micro to contain a unique serial number it retains its COM-port number between uses on the same computer. (The Teensy 4.0 work similarly out of the box?) But how do we identifying the same device on severals computers? Using the COM-port number won’t work as it changes between computers. However, the VID/PID combination and our unique product name stays the same. Using this knowledge and some Win32 wizardry we can enumerate all serial device currently connected to our computer and choose those we recognize.
Enumerating serial devices on Windows is a daunting task. There are libraries such as this one. It contains ten (!?) different ways to enumerate serial devices. For our purposes we don’t need ten; we only need one that works for USB serial devices.
This code enumerates all USB serial devices connected to the computer:
// Compiled and tested with Visual Studio 2017 15.9.38 and SDK 10.0.20348.0
#include <iostream>
#include <vector>
#include <Windows.h>
#include <SetupAPI.h>
#include <initguid.h> // Include before devpkey.h
#include <devpkey.h>
#pragma comment(lib, "SetupAPI.lib")
int main()
{
const GUID guid = GUID_DEVINTERFACE_COMPORT;
HDEVINFO devices = SetupDiGetClassDevs(
&guid,
0,
0,
DIGCF_DEVICEINTERFACE | DIGCF_PRESENT);
if (devices == INVALID_HANDLE_VALUE)
{
return 1; // Insert error handling here
}
for (DWORD device_index = 0; ; device_index++)
{
if (GetLastError() == ERROR_NO_MORE_ITEMS)
{
break; // No more devices to enumerate
}
std::wstring description;
{
SP_DEVINFO_DATA device_info = {};
device_info.cbSize = sizeof(SP_DEVINFO_DATA);
if (!SetupDiEnumDeviceInfo(devices, device_index, &device_info))
{
continue; // Insert error handling here
}
std::vector<WCHAR> buffer(1024);
// You may call this with zero length to get the required size
DWORD property_type;
DWORD required_size = 0;
if (!SetupDiGetDeviceProperty(
devices,
&device_info,
&DEVPKEY_Device_BusReportedDeviceDesc,
&property_type,
(PBYTE)buffer.data(),
(DWORD)buffer.size(),
&required_size,
0))
{
continue; // Insert error handling here
}
description = std::wstring(buffer.begin(), buffer.end());
}
std::wstring device_path;
{
SP_DEVICE_INTERFACE_DATA interface_data = {};
interface_data.cbSize = sizeof(interface_data);
if (!SetupDiEnumDeviceInterfaces(
devices,
0,
&guid,
device_index,
&interface_data))
{
continue; // Insert error handling here
}
std::vector<char> buffer(1024);
auto details = (PSP_DEVICE_INTERFACE_DETAIL_DATA)buffer.data();
details->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
// You may call this with zero length to get the required size
DWORD required_size = 0;
if (!SetupDiGetDeviceInterfaceDetail(
devices,
&interface_data,
details,
buffer.size(),
&required_size,
0))
{
continue; // Insert error handling here
}
device_path = details->DevicePath;
}
std::wcout
<< L"index=" << device_index
<< L", description=" << description.c_str()
<< L", path=" << device_path.c_str() << std::endl;
}
SetupDiDestroyDeviceInfoList(devices);
return 0;
}Running this code on my computer prints the following:
index=0, description=My Board, path=\\?\usb#vid_1b4f&pid_9206&mi_00#6&a14f7f3&3&0000#{86e0d1e0-8089-11d0-9ce4-08003e301f73}The string "My Board" is my product name from the device’s USB descriptor. Your device should use a longer more specific name to guarantee uniqueness.
The path "\\?\usb#vid_1b4f&pid_9206&..." uniquely identifies the device instance on my machine. It also includes the VID/PID combination for the Pro Micro (1B4F/9206.) According to Silabs there is a well defined structure to the device path. Note for example that there is no serial number in the device path above as the Pro Micro is a composite device. Its device path contains an instance ID instead of a serial number.
Please let me know if you find a way to reliably access the serial number and manufacturer string.
Uniquely identifying our devices is therefor a simple two step process:
If the answer is Yes to both these questions you can be reasonably sure this is your board. Next step is opening a line of communication.
Most tutorials seen online suggests finding the COM-port number then creating a file handle with CreateFile. However, we are going to ignore the COM-port number and instead use the device path directly.
// Create a file handle using the device path instead of a COM-port number. The
// Arduino will likely reset when we open the connection. I'm note sure if we
// can avoid this.
HANDLE handle = CreateFile(device_path.c_str(),
GENERIC_READ | GENERIC_WRITE, // read and write access
0, // no sharing
NULL, // default security attributes
OPEN_EXISTING, // opens existing pipe
FILE_ATTRIBUTE_NORMAL, // default attributes
NULL); // no template file
if (!handle || handle == INVALID_HANDLE_VALUE)
{
return false; // Insert error handling here
}
// See `SetCommState()` and the `DCB` struct if you are interested in baud rate
// settings and flow control. Since this is a virtual COM port we don't need to
// bother.
// Write some data
{
DWORD num_written = 0;
std::vector<uint8_t> buffer = { 'H', 'e', 'l', 'l', 'o' };
if (!WriteFile(handle, buffer.data(), buffer.size(), &num_written, NULL))
{
return false; // Insert error handling here
}
if (num_written < buffer.size())
{
return false; // Insert error handling here (for timeout)
}
}
// Read some data
{
DWORD num_read = 0;
std::vector<uint8_t> buffer;
// Get available bytes (and clear errors that would prevent further IO)
COMSTAT stats;
if (!ClearCommError(handle, NULL, &stats))
{
return false; // Insert error handling here
}
DWORD num_available = stats.cbInQue;
if (num_available > 0)
{
buffer.resize(num_available); // Allocate memory to hold what's read
if (!ReadFile(handle, buffer.data(), buffer.size(), &num_read, NULL))
{
return false; // Insert error handling here
}
buffer.resize(num_read);
if (num_read < num_available)
{
return false; // Insert error handling here (for timeout)
}
}
}