USB ReadFile() 超时替换串行通信设备使用的串行端口代码
time out for USB ReadFile() replacing Serial Port code used with serial communications device
我正在为热敏收据打印机扩展收据打印串行端口 (COM) 接口,以使用 USB 接口而不需要虚拟串行端口。我有一个工作原型,它将枚举连接的 USB 设备,找到具有特定供应商 ID 和产品 ID 的设备的 USB 路径,并使用 CreateFile()
.
打开与设备的连接
现有的串行端口代码使用Windows API 包裹在一组函数中。我采用的方法是使用同一组函数添加额外的代码,但这取决于 USB 连接而不是串行端口连接。我以前使用相同的方法允许通过串行端口或 WiFi/LAN 连接使用厨房打印机,并且成功地对现有代码进行了最小的更改。
不幸的是,使用函数库的现有代码依赖于使用 ReadFile()
指定超时的函数,这样如果热敏打印机在合理的时间内没有响应状态请求,应用程序可以将其标记为关闭并允许操作继续或使用备用打印机或辅助打印机。
如何为来自 CreateFile()
的文件句柄上的 ReadFile()
指定超时,该句柄使用 USB 路径名打开到通信设备的连接?
需要考虑的是,这是用于多个串行通信设备(收据打印机、厨房打印机、秤等)的多线程代码,但是一个线程将独占访问特定设备(厨房打印功能打开串行端口仅用于厨房打印机,秤读取功能打开串行端口仅用于秤等)。
现有串口代码中,用于设置超时的函数,SetCommTimeouts()
, for a Serial Port connection opened with CreateFile()
does not work for a USB connection opened with CreateFile()
(see SetupComm, SetCommState, SetCommTimeouts fail with USB device)。这意味着需要一些其他机制来提供一种方法来允许 I/O 由于使用 USB 设备路径名时超时而导致的失败。
我们正在使用以下代码段打开串行端口,无论是硬件 COM 端口还是模拟硬件 COM 端口的虚拟串行端口:
// see Microsoft document HOWTO: Specify Serial Ports Larger than COM9.
// https://support.microsoft.com/en-us/kb/115831
// CreateFile() can be used to get a handle to a serial port. The "Win32 Programmer's Reference" entry for "CreateFile()"
// mentions that the share mode must be 0, the create parameter must be OPEN_EXISTING, and the template must be NULL.
//
// CreateFile() is successful when you use "COM1" through "COM9" for the name of the file;
// however, the value INVALID_HANDLE_VALUE is returned if you use "COM10" or greater.
//
// If the name of the port is \.\COM10, the correct way to specify the serial port in a call to
// CreateFile() is "\\.\COM10".
//
// NOTES: This syntax also works for ports COM1 through COM9. Certain boards will let you choose
// the port names yourself. This syntax works for those names as well.
wsprintf(wszPortName, TEXT("\\.\COM%d"), usPortId);
/* Open the serial port. */
/* avoid to failuer of CreateFile */
for (i = 0; i < 10; i++) {
hHandle = CreateFile (wszPortName, /* Pointer to the name of the port, PifOpenCom() */
GENERIC_READ | GENERIC_WRITE, /* Access (read-write) mode */
0, /* Share mode */
NULL, /* Pointer to the security attribute */
OPEN_EXISTING,/* How to open the serial port */
0, /* Port attributes */
NULL); /* Handle to port with attribute */
/* to copy */
/* If it fails to open the port, return FALSE. */
if ( hHandle == INVALID_HANDLE_VALUE ) { /* Could not open the port. */
dwError = GetLastError ();
if (dwError == ERROR_FILE_NOT_FOUND || dwError == ERROR_INVALID_NAME || dwError == ERROR_ACCESS_DENIED) {
LeaveCriticalSection(&g_SioCriticalSection);
// the COM port does not exist. probably a Virtual Serial Communications Port
// from a USB device which was either unplugged or turned off.
// or the COM port or Virtual Serial Communications port is in use by some other application.
return PIF_ERROR_COM_ACCESS_DENIED;
}
PifLog (MODULE_PIF_OPENCOM, LOG_ERROR_PIFSIO_CODE_01);
PifLog (MODULE_ERROR_NO(MODULE_PIF_OPENCOM), (USHORT)dwError);
PifLog(MODULE_DATA_VALUE(FAULT_AT_PIFOPENCOM), usPortId);
PifSleep(500);
} else {
break;
}
}
if ( hHandle == INVALID_HANDLE_VALUE ) { /* Could not open the port. */
wsprintf(wszDisplay, TEXT("CreateFile, COM%d, Last Error =%d\n"), usPortId, dwError);
OutputDebugString(wszDisplay);
LeaveCriticalSection(&g_SioCriticalSection);
return PIF_ERROR_COM_ERRORS;
}
/* clear the error and purge the receive buffer */
dwError = (DWORD)(~0); // set all error code bits on
ClearCommError(hHandle, &dwError, NULL);
PurgeComm( hHandle, PURGE_TXABORT | PURGE_RXABORT | PURGE_TXCLEAR | PURGE_RXCLEAR ) ;
ReadFile()
包含在一个函数中,看起来像:
fResult = ReadFile(hHandle, pBuffer, (DWORD)usBytes, &dwBytesRead, NULL);
if (PifSioCheckPowerDown(usPort, aPifSioTable) == TRUE) {
return PIF_ERROR_COM_POWER_FAILURE;
}
if (fResult) {
if (!dwBytesRead) return PIF_ERROR_COM_TIMEOUT;
return (SHORT)dwBytesRead;
} else {
SHORT sErrorCode = 0; // error code from PifSubGetErrorCode(). must call after GetLastError().
dwError = GetLastError();
PifLog (MODULE_PIF_READCOM, LOG_ERROR_PIFSIO_CODE_06);
PifLog (MODULE_ERROR_NO(MODULE_PIF_READCOM), (USHORT)dwError);
sErrorCode = PifSubGetErrorCode(hHandle);
PifLog (MODULE_ERROR_NO(MODULE_PIF_READCOM), (USHORT)abs(sErrorCode));
PifLog (MODULE_DATA_VALUE(MODULE_PIF_READCOM), usPort);
return (sErrorCode);
}
我发现了许多类似的问题,其中涉及管道,但是使用重叠 I/O 的相同方法适用。
- Breaking ReadFile() blocking - Named Pipe (Windows API)
- Win32 API: ReadFile timeout
- Device driver: Windows ReadFile function timeout
我还在网上找到了以下文章 Peter's blog: Getting a handle on usbprint.sys,其中提供了有关如何查找 USB 连接设备的 USB 路径名的代码和说明。我在下面的 class 中使用了一些代码示例。
我还在 codeproject.com、Enumerating windows device 上找到了 Chuan-Liang Teng 的一篇文章,其中包含一个枚举连接的 USB 设备并询问设备的各种设置和详细信息的示例。那篇文章中的代码虽然很旧,但对这个特定的应用程序来说不是必需的,但很有帮助。
我有一个使用重叠 I/O 的 C++ 原型 class,它似乎复制了使用 USB 连接到热敏打印机的串行端口连接所看到的行为。完整的源代码和 Visual Studio 2017 解决方案和项目文件在我的 GitHub 存储库 https://github.com/RichardChambers/utilities_tools/tree/main/UsbWindows 中,因为此片段具有最相关的部分。
我已经在销售点应用程序中使用修改后的代码进行了简单测试,现在正在将其集成到现有的热敏收据打印机源代码中,该打印机源代码已经可以与串行端口一起使用。
#include <windows.h>
#include <setupapi.h>
#include <initguid.h>
#include <iostream>
// This is the GUID for the USB device class.
// It is defined in the include file Usbiodef.h of the Microsoft Windows Driver Kit.
// See also https://msdn.microsoft.com/en-us/library/windows/hardware/ff545972(v=vs.85).aspx which
// provides basic documentation on this GUID.
DEFINE_GUID(GUID_DEVINTERFACE_USB_DEVICE, 0xA5DCBF10L, 0x6530, 0x11D2, 0x90, 0x1F, 0x00, 0xC0, 0x4F, 0xB9, 0x51, 0xED);
class UsbSerialDevice
{
public:
// See https://docs.microsoft.com/en-us/windows/win32/ipc/named-pipe-server-using-overlapped-i-o?redirectedfrom=MSDN
// to implement time outs for Write and for Read.
UsbSerialDevice(const wchar_t* wszVendorIdIn = nullptr);
~UsbSerialDevice();
int CreateEndPoint(const wchar_t* wszVendorId = nullptr, DWORD dwDesiredAccess = (GENERIC_READ | GENERIC_WRITE));
void CloseEndPoint(void);
int ListEndPoint(const wchar_t* wszVendorIdIn);
int ReadStream(void* bString, size_t nBytes);
int WriteStream(void* bString, size_t nBytes);
DWORD SetWriteTimeOut(DWORD msTimeout);
DWORD SetReadTimeOut(DWORD msTimeout);
DWORD m_dwError; // GetLastError() for last action
DWORD m_dwErrorWrite; // GetLastError() for last write
DWORD m_dwErrorRead; // GetLastError() for last read
DWORD m_dwBytesWritten; // number of bytes last write
DWORD m_dwBytesRead; // number of bytes last read
DWORD m_dwWait; // WaitForSingleObject() return value
private:
HANDLE m_hFile;
OVERLAPPED m_oOverlap;
COMMTIMEOUTS m_timeOut;
const unsigned short m_idLen = 255;
wchar_t m_wszVendorId[255 + 1] = { 0 };
};
UsbSerialDevice::UsbSerialDevice(const wchar_t* wszVendorIdIn) :
m_dwError(0),
m_dwErrorWrite(0),
m_dwErrorRead(0),
m_dwBytesWritten(0),
m_dwBytesRead(0),
m_dwWait(0),
m_hFile(INVALID_HANDLE_VALUE)
{
memset(&m_oOverlap, 0, sizeof(m_oOverlap));
m_oOverlap.hEvent = INVALID_HANDLE_VALUE;
if (wszVendorIdIn != nullptr) ListEndPoint(wszVendorIdIn);
}
void UsbSerialDevice::CloseEndPoint(void )
{
if (m_hFile && m_hFile != INVALID_HANDLE_VALUE) CloseHandle(m_hFile);
if (m_oOverlap.hEvent && m_oOverlap.hEvent != INVALID_HANDLE_VALUE) CloseHandle(m_oOverlap.hEvent);
}
UsbSerialDevice::~UsbSerialDevice()
{
CloseEndPoint();
}
/*
* Returns: -1 - file handle is invalid
* 0 - write failed. See m_dwErrorWrite for GetLastError() value
* 1 - write succedded.
*/
int UsbSerialDevice::WriteStream(void* bString, size_t nBytes)
{
SetLastError(0);
m_dwError = m_dwErrorWrite = 0;
m_dwBytesWritten = 0;
m_dwWait = WAIT_FAILED;
if (m_hFile && m_hFile != INVALID_HANDLE_VALUE) {
BOOL bWrite = WriteFile(m_hFile, bString, nBytes, 0, &m_oOverlap);
m_dwError = m_dwErrorWrite = GetLastError();
if (!bWrite && m_dwError == ERROR_IO_PENDING) {
SetLastError(0);
m_dwError = m_dwErrorWrite = 0;
m_dwWait = WaitForSingleObject(m_oOverlap.hEvent, m_timeOut.WriteTotalTimeoutConstant);
BOOL bCancel = FALSE;
switch (m_dwWait) {
case WAIT_OBJECT_0: // The state of the specified object is signaled.
break;
case WAIT_FAILED: // The function has failed. To get extended error information, call GetLastError.
m_dwError = m_dwErrorWrite = GetLastError();
bCancel = CancelIo(m_hFile);
break;
case WAIT_TIMEOUT: // The time-out interval elapsed, and the object's state is nonsignaled.
case WAIT_ABANDONED: // thread owning mutex terminated before releasing or signaling object.
bCancel = CancelIo(m_hFile);
m_dwError = m_dwErrorRead = ERROR_COUNTER_TIMEOUT;
break;
}
bWrite = GetOverlappedResult(m_hFile, &m_oOverlap, &m_dwBytesRead, FALSE);
}
return bWrite; // 0 or FALSE if failed, 1 or TRUE if succeeded.
}
return -1;
}
/*
* Returns: -1 - file handle is invalid
* 0 - read failed. See m_dwErrorRead for GetLastError() value
* 1 - read succedded.
*/
int UsbSerialDevice::ReadStream(void* bString, size_t nBytes)
{
SetLastError(0);
m_dwError = m_dwErrorRead = 0;
m_dwBytesRead = 0;
m_dwWait = WAIT_FAILED;
if (m_hFile && m_hFile != INVALID_HANDLE_VALUE) {
BOOL bRead = ReadFile(m_hFile, bString, nBytes, &m_dwBytesRead, &m_oOverlap);
m_dwError = m_dwErrorRead = GetLastError();
if (!bRead && m_dwError == ERROR_IO_PENDING) {
SetLastError(0);
m_dwError = m_dwErrorRead = 0;
m_dwWait = WaitForSingleObject(m_oOverlap.hEvent, m_timeOut.ReadTotalTimeoutConstant);
BOOL bCancel = FALSE;
switch (m_dwWait) {
case WAIT_OBJECT_0: // The state of the specified object is signaled.
break;
case WAIT_FAILED: // The function has failed. To get extended error information, call GetLastError.
m_dwError = m_dwErrorWrite = GetLastError();
bCancel = CancelIo(m_hFile);
break;
case WAIT_TIMEOUT: // The time-out interval elapsed, and the object's state is nonsignaled.
case WAIT_ABANDONED: // thread owning mutex terminated before releasing or signaling object.
bCancel = CancelIo(m_hFile);
m_dwError = m_dwErrorRead = ERROR_COUNTER_TIMEOUT;
break;
}
bRead = GetOverlappedResult(m_hFile, &m_oOverlap, &m_dwBytesRead, FALSE);
}
return bRead; // 0 or FALSE if failed, 1 or TRUE if succeeded.
}
return -1;
}
int UsbSerialDevice::ListEndPoint(const wchar_t* wszVendorIdIn)
{
m_dwError = ERROR_INVALID_HANDLE;
if (wszVendorIdIn == nullptr) return 0;
HDEVINFO hDevInfo;
// we need to make sure the vendor and product id codes are in lower case
// as this is needed for the CreateFile() function to open the connection
// to the USB device correctly. this lower case conversion applies to
// any alphabetic characters in the identifier.
//
// for example "VID_0FE6&PID_811E" must be converted to "vid_0fe6&pid_811e"
wchar_t wszVendorId[256] = { 0 };
for (unsigned short i = 0; i < 255 && (wszVendorId[i] = towlower(wszVendorIdIn[i])); i++);
// We will try to get device information set for all USB devices that have a
// device interface and are currently present on the system (plugged in).
hDevInfo = SetupDiGetClassDevs(&GUID_DEVINTERFACE_USB_DEVICE, NULL, 0, DIGCF_DEVICEINTERFACE | DIGCF_PRESENT);
if (hDevInfo != INVALID_HANDLE_VALUE)
{
DWORD dwMemberIdx;
BOOL bContinue = TRUE;
SP_DEVICE_INTERFACE_DATA DevIntfData;
// Prepare to enumerate all device interfaces for the device information
// set that we retrieved with SetupDiGetClassDevs(..)
DevIntfData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
dwMemberIdx = 0;
// Next, we will keep calling this SetupDiEnumDeviceInterfaces(..) until this
// function causes GetLastError() to return ERROR_NO_MORE_ITEMS. With each
// call the dwMemberIdx value needs to be incremented to retrieve the next
// device interface information.
for (BOOL bContinue = TRUE; bContinue; ) {
PSP_DEVICE_INTERFACE_DETAIL_DATA DevIntfDetailData;
SP_DEVINFO_DATA DevData;
DWORD dwSize;
dwMemberIdx++;
SetupDiEnumDeviceInterfaces(hDevInfo, NULL, &GUID_DEVINTERFACE_USB_DEVICE, dwMemberIdx, &DevIntfData);
if (GetLastError() == ERROR_NO_MORE_ITEMS) break;
// As a last step we will need to get some more details for each
// of device interface information we are able to retrieve. This
// device interface detail gives us the information we need to identify
// the device (VID/PID), and decide if it's useful to us. It will also
// provide a DEVINFO_DATA structure which we can use to know the serial
// port name for a virtual com port.
DevData.cbSize = sizeof(DevData);
// Get the required buffer size. Call SetupDiGetDeviceInterfaceDetail with
// a NULL DevIntfDetailData pointer, a DevIntfDetailDataSize
// of zero, and a valid RequiredSize variable. In response to such a call,
// this function returns the required buffer size at dwSize.
SetupDiGetDeviceInterfaceDetail(hDevInfo, &DevIntfData, NULL, 0, &dwSize, NULL);
// Allocate memory for the DeviceInterfaceDetail struct. Don't forget to
// deallocate it later!
DevIntfDetailData = (PSP_DEVICE_INTERFACE_DETAIL_DATA)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwSize);
DevIntfDetailData->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
if (SetupDiGetDeviceInterfaceDetail(hDevInfo, &DevIntfData, DevIntfDetailData, dwSize, &dwSize, &DevData))
{
if (wcsstr(DevIntfDetailData->DevicePath, wszVendorId)) {
wcscpy_s(m_wszVendorId, DevIntfDetailData->DevicePath);
}
}
HeapFree(GetProcessHeap(), 0, DevIntfDetailData);
}
SetupDiDestroyDeviceInfoList(hDevInfo);
}
return 0;
}
int UsbSerialDevice::CreateEndPoint(const wchar_t* wszVendorIdIn, DWORD dwDesiredAccess)
{
if (wszVendorIdIn) {
ListEndPoint(wszVendorIdIn);
}
m_dwError = ERROR_INVALID_HANDLE;
// Finally we can start checking if we've found a useable device,
// by inspecting the DevIntfDetailData->DevicePath variable.
//
// The DevicePath looks something like this for a Brecknell 67xx Series Serial Scale
// \?\usb#vid_1a86&pid_7523#6&28eaabda&0&2#{a5dcbf10-6530-11d2-901f-00c04fb951ed}
//
// The VID for a particular vendor will be the same for a particular vendor's equipment.
// The PID is variable for each device of the vendor.
//
// As you can see it contains the VID/PID for the device, so we can check
// for the right VID/PID with string handling routines.
// See https://github.com/Microsoft/Windows-driver-samples/blob/master/usb/usbview/vndrlist.h
// See https://blog.peter.skarpetis.com/archives/2005/04/07/getting-a-handle-on-usbprintsys/
// which describes a sample USB thermal receipt printer test application.
SetLastError(0);
m_hFile = CreateFile(m_wszVendorId, dwDesiredAccess, FILE_SHARE_READ | FILE_SHARE_WRITE, 0, OPEN_ALWAYS, FILE_FLAG_OVERLAPPED, 0);
if (m_hFile == INVALID_HANDLE_VALUE) {
m_dwError = GetLastError();
// wprintf(_T(" CreateFile() failed. GetLastError() = %d\n"), m_dwError);
}
else {
m_oOverlap.hEvent = CreateEvent(
NULL, // default security attribute
TRUE, // manual-reset event
TRUE, // initial state = signaled
NULL); // unnamed event object
m_timeOut.ReadIntervalTimeout = 0;
m_timeOut.ReadTotalTimeoutMultiplier = 0;
m_timeOut.ReadTotalTimeoutConstant = 5000;
m_timeOut.WriteTotalTimeoutMultiplier = 0;
m_timeOut.WriteTotalTimeoutConstant = 5000;
m_dwError = 0; // GetLastError();
return 1;
}
return 0;
}
我正在为热敏收据打印机扩展收据打印串行端口 (COM) 接口,以使用 USB 接口而不需要虚拟串行端口。我有一个工作原型,它将枚举连接的 USB 设备,找到具有特定供应商 ID 和产品 ID 的设备的 USB 路径,并使用 CreateFile()
.
现有的串行端口代码使用Windows API 包裹在一组函数中。我采用的方法是使用同一组函数添加额外的代码,但这取决于 USB 连接而不是串行端口连接。我以前使用相同的方法允许通过串行端口或 WiFi/LAN 连接使用厨房打印机,并且成功地对现有代码进行了最小的更改。
不幸的是,使用函数库的现有代码依赖于使用 ReadFile()
指定超时的函数,这样如果热敏打印机在合理的时间内没有响应状态请求,应用程序可以将其标记为关闭并允许操作继续或使用备用打印机或辅助打印机。
如何为来自 CreateFile()
的文件句柄上的 ReadFile()
指定超时,该句柄使用 USB 路径名打开到通信设备的连接?
需要考虑的是,这是用于多个串行通信设备(收据打印机、厨房打印机、秤等)的多线程代码,但是一个线程将独占访问特定设备(厨房打印功能打开串行端口仅用于厨房打印机,秤读取功能打开串行端口仅用于秤等)。
现有串口代码中,用于设置超时的函数,SetCommTimeouts()
, for a Serial Port connection opened with CreateFile()
does not work for a USB connection opened with CreateFile()
(see SetupComm, SetCommState, SetCommTimeouts fail with USB device)。这意味着需要一些其他机制来提供一种方法来允许 I/O 由于使用 USB 设备路径名时超时而导致的失败。
我们正在使用以下代码段打开串行端口,无论是硬件 COM 端口还是模拟硬件 COM 端口的虚拟串行端口:
// see Microsoft document HOWTO: Specify Serial Ports Larger than COM9.
// https://support.microsoft.com/en-us/kb/115831
// CreateFile() can be used to get a handle to a serial port. The "Win32 Programmer's Reference" entry for "CreateFile()"
// mentions that the share mode must be 0, the create parameter must be OPEN_EXISTING, and the template must be NULL.
//
// CreateFile() is successful when you use "COM1" through "COM9" for the name of the file;
// however, the value INVALID_HANDLE_VALUE is returned if you use "COM10" or greater.
//
// If the name of the port is \.\COM10, the correct way to specify the serial port in a call to
// CreateFile() is "\\.\COM10".
//
// NOTES: This syntax also works for ports COM1 through COM9. Certain boards will let you choose
// the port names yourself. This syntax works for those names as well.
wsprintf(wszPortName, TEXT("\\.\COM%d"), usPortId);
/* Open the serial port. */
/* avoid to failuer of CreateFile */
for (i = 0; i < 10; i++) {
hHandle = CreateFile (wszPortName, /* Pointer to the name of the port, PifOpenCom() */
GENERIC_READ | GENERIC_WRITE, /* Access (read-write) mode */
0, /* Share mode */
NULL, /* Pointer to the security attribute */
OPEN_EXISTING,/* How to open the serial port */
0, /* Port attributes */
NULL); /* Handle to port with attribute */
/* to copy */
/* If it fails to open the port, return FALSE. */
if ( hHandle == INVALID_HANDLE_VALUE ) { /* Could not open the port. */
dwError = GetLastError ();
if (dwError == ERROR_FILE_NOT_FOUND || dwError == ERROR_INVALID_NAME || dwError == ERROR_ACCESS_DENIED) {
LeaveCriticalSection(&g_SioCriticalSection);
// the COM port does not exist. probably a Virtual Serial Communications Port
// from a USB device which was either unplugged or turned off.
// or the COM port or Virtual Serial Communications port is in use by some other application.
return PIF_ERROR_COM_ACCESS_DENIED;
}
PifLog (MODULE_PIF_OPENCOM, LOG_ERROR_PIFSIO_CODE_01);
PifLog (MODULE_ERROR_NO(MODULE_PIF_OPENCOM), (USHORT)dwError);
PifLog(MODULE_DATA_VALUE(FAULT_AT_PIFOPENCOM), usPortId);
PifSleep(500);
} else {
break;
}
}
if ( hHandle == INVALID_HANDLE_VALUE ) { /* Could not open the port. */
wsprintf(wszDisplay, TEXT("CreateFile, COM%d, Last Error =%d\n"), usPortId, dwError);
OutputDebugString(wszDisplay);
LeaveCriticalSection(&g_SioCriticalSection);
return PIF_ERROR_COM_ERRORS;
}
/* clear the error and purge the receive buffer */
dwError = (DWORD)(~0); // set all error code bits on
ClearCommError(hHandle, &dwError, NULL);
PurgeComm( hHandle, PURGE_TXABORT | PURGE_RXABORT | PURGE_TXCLEAR | PURGE_RXCLEAR ) ;
ReadFile()
包含在一个函数中,看起来像:
fResult = ReadFile(hHandle, pBuffer, (DWORD)usBytes, &dwBytesRead, NULL);
if (PifSioCheckPowerDown(usPort, aPifSioTable) == TRUE) {
return PIF_ERROR_COM_POWER_FAILURE;
}
if (fResult) {
if (!dwBytesRead) return PIF_ERROR_COM_TIMEOUT;
return (SHORT)dwBytesRead;
} else {
SHORT sErrorCode = 0; // error code from PifSubGetErrorCode(). must call after GetLastError().
dwError = GetLastError();
PifLog (MODULE_PIF_READCOM, LOG_ERROR_PIFSIO_CODE_06);
PifLog (MODULE_ERROR_NO(MODULE_PIF_READCOM), (USHORT)dwError);
sErrorCode = PifSubGetErrorCode(hHandle);
PifLog (MODULE_ERROR_NO(MODULE_PIF_READCOM), (USHORT)abs(sErrorCode));
PifLog (MODULE_DATA_VALUE(MODULE_PIF_READCOM), usPort);
return (sErrorCode);
}
我发现了许多类似的问题,其中涉及管道,但是使用重叠 I/O 的相同方法适用。
- Breaking ReadFile() blocking - Named Pipe (Windows API)
- Win32 API: ReadFile timeout
- Device driver: Windows ReadFile function timeout
我还在网上找到了以下文章 Peter's blog: Getting a handle on usbprint.sys,其中提供了有关如何查找 USB 连接设备的 USB 路径名的代码和说明。我在下面的 class 中使用了一些代码示例。
我还在 codeproject.com、Enumerating windows device 上找到了 Chuan-Liang Teng 的一篇文章,其中包含一个枚举连接的 USB 设备并询问设备的各种设置和详细信息的示例。那篇文章中的代码虽然很旧,但对这个特定的应用程序来说不是必需的,但很有帮助。
我有一个使用重叠 I/O 的 C++ 原型 class,它似乎复制了使用 USB 连接到热敏打印机的串行端口连接所看到的行为。完整的源代码和 Visual Studio 2017 解决方案和项目文件在我的 GitHub 存储库 https://github.com/RichardChambers/utilities_tools/tree/main/UsbWindows 中,因为此片段具有最相关的部分。
我已经在销售点应用程序中使用修改后的代码进行了简单测试,现在正在将其集成到现有的热敏收据打印机源代码中,该打印机源代码已经可以与串行端口一起使用。
#include <windows.h>
#include <setupapi.h>
#include <initguid.h>
#include <iostream>
// This is the GUID for the USB device class.
// It is defined in the include file Usbiodef.h of the Microsoft Windows Driver Kit.
// See also https://msdn.microsoft.com/en-us/library/windows/hardware/ff545972(v=vs.85).aspx which
// provides basic documentation on this GUID.
DEFINE_GUID(GUID_DEVINTERFACE_USB_DEVICE, 0xA5DCBF10L, 0x6530, 0x11D2, 0x90, 0x1F, 0x00, 0xC0, 0x4F, 0xB9, 0x51, 0xED);
class UsbSerialDevice
{
public:
// See https://docs.microsoft.com/en-us/windows/win32/ipc/named-pipe-server-using-overlapped-i-o?redirectedfrom=MSDN
// to implement time outs for Write and for Read.
UsbSerialDevice(const wchar_t* wszVendorIdIn = nullptr);
~UsbSerialDevice();
int CreateEndPoint(const wchar_t* wszVendorId = nullptr, DWORD dwDesiredAccess = (GENERIC_READ | GENERIC_WRITE));
void CloseEndPoint(void);
int ListEndPoint(const wchar_t* wszVendorIdIn);
int ReadStream(void* bString, size_t nBytes);
int WriteStream(void* bString, size_t nBytes);
DWORD SetWriteTimeOut(DWORD msTimeout);
DWORD SetReadTimeOut(DWORD msTimeout);
DWORD m_dwError; // GetLastError() for last action
DWORD m_dwErrorWrite; // GetLastError() for last write
DWORD m_dwErrorRead; // GetLastError() for last read
DWORD m_dwBytesWritten; // number of bytes last write
DWORD m_dwBytesRead; // number of bytes last read
DWORD m_dwWait; // WaitForSingleObject() return value
private:
HANDLE m_hFile;
OVERLAPPED m_oOverlap;
COMMTIMEOUTS m_timeOut;
const unsigned short m_idLen = 255;
wchar_t m_wszVendorId[255 + 1] = { 0 };
};
UsbSerialDevice::UsbSerialDevice(const wchar_t* wszVendorIdIn) :
m_dwError(0),
m_dwErrorWrite(0),
m_dwErrorRead(0),
m_dwBytesWritten(0),
m_dwBytesRead(0),
m_dwWait(0),
m_hFile(INVALID_HANDLE_VALUE)
{
memset(&m_oOverlap, 0, sizeof(m_oOverlap));
m_oOverlap.hEvent = INVALID_HANDLE_VALUE;
if (wszVendorIdIn != nullptr) ListEndPoint(wszVendorIdIn);
}
void UsbSerialDevice::CloseEndPoint(void )
{
if (m_hFile && m_hFile != INVALID_HANDLE_VALUE) CloseHandle(m_hFile);
if (m_oOverlap.hEvent && m_oOverlap.hEvent != INVALID_HANDLE_VALUE) CloseHandle(m_oOverlap.hEvent);
}
UsbSerialDevice::~UsbSerialDevice()
{
CloseEndPoint();
}
/*
* Returns: -1 - file handle is invalid
* 0 - write failed. See m_dwErrorWrite for GetLastError() value
* 1 - write succedded.
*/
int UsbSerialDevice::WriteStream(void* bString, size_t nBytes)
{
SetLastError(0);
m_dwError = m_dwErrorWrite = 0;
m_dwBytesWritten = 0;
m_dwWait = WAIT_FAILED;
if (m_hFile && m_hFile != INVALID_HANDLE_VALUE) {
BOOL bWrite = WriteFile(m_hFile, bString, nBytes, 0, &m_oOverlap);
m_dwError = m_dwErrorWrite = GetLastError();
if (!bWrite && m_dwError == ERROR_IO_PENDING) {
SetLastError(0);
m_dwError = m_dwErrorWrite = 0;
m_dwWait = WaitForSingleObject(m_oOverlap.hEvent, m_timeOut.WriteTotalTimeoutConstant);
BOOL bCancel = FALSE;
switch (m_dwWait) {
case WAIT_OBJECT_0: // The state of the specified object is signaled.
break;
case WAIT_FAILED: // The function has failed. To get extended error information, call GetLastError.
m_dwError = m_dwErrorWrite = GetLastError();
bCancel = CancelIo(m_hFile);
break;
case WAIT_TIMEOUT: // The time-out interval elapsed, and the object's state is nonsignaled.
case WAIT_ABANDONED: // thread owning mutex terminated before releasing or signaling object.
bCancel = CancelIo(m_hFile);
m_dwError = m_dwErrorRead = ERROR_COUNTER_TIMEOUT;
break;
}
bWrite = GetOverlappedResult(m_hFile, &m_oOverlap, &m_dwBytesRead, FALSE);
}
return bWrite; // 0 or FALSE if failed, 1 or TRUE if succeeded.
}
return -1;
}
/*
* Returns: -1 - file handle is invalid
* 0 - read failed. See m_dwErrorRead for GetLastError() value
* 1 - read succedded.
*/
int UsbSerialDevice::ReadStream(void* bString, size_t nBytes)
{
SetLastError(0);
m_dwError = m_dwErrorRead = 0;
m_dwBytesRead = 0;
m_dwWait = WAIT_FAILED;
if (m_hFile && m_hFile != INVALID_HANDLE_VALUE) {
BOOL bRead = ReadFile(m_hFile, bString, nBytes, &m_dwBytesRead, &m_oOverlap);
m_dwError = m_dwErrorRead = GetLastError();
if (!bRead && m_dwError == ERROR_IO_PENDING) {
SetLastError(0);
m_dwError = m_dwErrorRead = 0;
m_dwWait = WaitForSingleObject(m_oOverlap.hEvent, m_timeOut.ReadTotalTimeoutConstant);
BOOL bCancel = FALSE;
switch (m_dwWait) {
case WAIT_OBJECT_0: // The state of the specified object is signaled.
break;
case WAIT_FAILED: // The function has failed. To get extended error information, call GetLastError.
m_dwError = m_dwErrorWrite = GetLastError();
bCancel = CancelIo(m_hFile);
break;
case WAIT_TIMEOUT: // The time-out interval elapsed, and the object's state is nonsignaled.
case WAIT_ABANDONED: // thread owning mutex terminated before releasing or signaling object.
bCancel = CancelIo(m_hFile);
m_dwError = m_dwErrorRead = ERROR_COUNTER_TIMEOUT;
break;
}
bRead = GetOverlappedResult(m_hFile, &m_oOverlap, &m_dwBytesRead, FALSE);
}
return bRead; // 0 or FALSE if failed, 1 or TRUE if succeeded.
}
return -1;
}
int UsbSerialDevice::ListEndPoint(const wchar_t* wszVendorIdIn)
{
m_dwError = ERROR_INVALID_HANDLE;
if (wszVendorIdIn == nullptr) return 0;
HDEVINFO hDevInfo;
// we need to make sure the vendor and product id codes are in lower case
// as this is needed for the CreateFile() function to open the connection
// to the USB device correctly. this lower case conversion applies to
// any alphabetic characters in the identifier.
//
// for example "VID_0FE6&PID_811E" must be converted to "vid_0fe6&pid_811e"
wchar_t wszVendorId[256] = { 0 };
for (unsigned short i = 0; i < 255 && (wszVendorId[i] = towlower(wszVendorIdIn[i])); i++);
// We will try to get device information set for all USB devices that have a
// device interface and are currently present on the system (plugged in).
hDevInfo = SetupDiGetClassDevs(&GUID_DEVINTERFACE_USB_DEVICE, NULL, 0, DIGCF_DEVICEINTERFACE | DIGCF_PRESENT);
if (hDevInfo != INVALID_HANDLE_VALUE)
{
DWORD dwMemberIdx;
BOOL bContinue = TRUE;
SP_DEVICE_INTERFACE_DATA DevIntfData;
// Prepare to enumerate all device interfaces for the device information
// set that we retrieved with SetupDiGetClassDevs(..)
DevIntfData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
dwMemberIdx = 0;
// Next, we will keep calling this SetupDiEnumDeviceInterfaces(..) until this
// function causes GetLastError() to return ERROR_NO_MORE_ITEMS. With each
// call the dwMemberIdx value needs to be incremented to retrieve the next
// device interface information.
for (BOOL bContinue = TRUE; bContinue; ) {
PSP_DEVICE_INTERFACE_DETAIL_DATA DevIntfDetailData;
SP_DEVINFO_DATA DevData;
DWORD dwSize;
dwMemberIdx++;
SetupDiEnumDeviceInterfaces(hDevInfo, NULL, &GUID_DEVINTERFACE_USB_DEVICE, dwMemberIdx, &DevIntfData);
if (GetLastError() == ERROR_NO_MORE_ITEMS) break;
// As a last step we will need to get some more details for each
// of device interface information we are able to retrieve. This
// device interface detail gives us the information we need to identify
// the device (VID/PID), and decide if it's useful to us. It will also
// provide a DEVINFO_DATA structure which we can use to know the serial
// port name for a virtual com port.
DevData.cbSize = sizeof(DevData);
// Get the required buffer size. Call SetupDiGetDeviceInterfaceDetail with
// a NULL DevIntfDetailData pointer, a DevIntfDetailDataSize
// of zero, and a valid RequiredSize variable. In response to such a call,
// this function returns the required buffer size at dwSize.
SetupDiGetDeviceInterfaceDetail(hDevInfo, &DevIntfData, NULL, 0, &dwSize, NULL);
// Allocate memory for the DeviceInterfaceDetail struct. Don't forget to
// deallocate it later!
DevIntfDetailData = (PSP_DEVICE_INTERFACE_DETAIL_DATA)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwSize);
DevIntfDetailData->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
if (SetupDiGetDeviceInterfaceDetail(hDevInfo, &DevIntfData, DevIntfDetailData, dwSize, &dwSize, &DevData))
{
if (wcsstr(DevIntfDetailData->DevicePath, wszVendorId)) {
wcscpy_s(m_wszVendorId, DevIntfDetailData->DevicePath);
}
}
HeapFree(GetProcessHeap(), 0, DevIntfDetailData);
}
SetupDiDestroyDeviceInfoList(hDevInfo);
}
return 0;
}
int UsbSerialDevice::CreateEndPoint(const wchar_t* wszVendorIdIn, DWORD dwDesiredAccess)
{
if (wszVendorIdIn) {
ListEndPoint(wszVendorIdIn);
}
m_dwError = ERROR_INVALID_HANDLE;
// Finally we can start checking if we've found a useable device,
// by inspecting the DevIntfDetailData->DevicePath variable.
//
// The DevicePath looks something like this for a Brecknell 67xx Series Serial Scale
// \?\usb#vid_1a86&pid_7523#6&28eaabda&0&2#{a5dcbf10-6530-11d2-901f-00c04fb951ed}
//
// The VID for a particular vendor will be the same for a particular vendor's equipment.
// The PID is variable for each device of the vendor.
//
// As you can see it contains the VID/PID for the device, so we can check
// for the right VID/PID with string handling routines.
// See https://github.com/Microsoft/Windows-driver-samples/blob/master/usb/usbview/vndrlist.h
// See https://blog.peter.skarpetis.com/archives/2005/04/07/getting-a-handle-on-usbprintsys/
// which describes a sample USB thermal receipt printer test application.
SetLastError(0);
m_hFile = CreateFile(m_wszVendorId, dwDesiredAccess, FILE_SHARE_READ | FILE_SHARE_WRITE, 0, OPEN_ALWAYS, FILE_FLAG_OVERLAPPED, 0);
if (m_hFile == INVALID_HANDLE_VALUE) {
m_dwError = GetLastError();
// wprintf(_T(" CreateFile() failed. GetLastError() = %d\n"), m_dwError);
}
else {
m_oOverlap.hEvent = CreateEvent(
NULL, // default security attribute
TRUE, // manual-reset event
TRUE, // initial state = signaled
NULL); // unnamed event object
m_timeOut.ReadIntervalTimeout = 0;
m_timeOut.ReadTotalTimeoutMultiplier = 0;
m_timeOut.ReadTotalTimeoutConstant = 5000;
m_timeOut.WriteTotalTimeoutMultiplier = 0;
m_timeOut.WriteTotalTimeoutConstant = 5000;
m_dwError = 0; // GetLastError();
return 1;
}
return 0;
}