Home > Back-end >  time out for USB ReadFile() replacing Serial Port code used with serial communications device
time out for USB ReadFile() replacing Serial Port code used with serial communications device

Time:11-07

I'm working on extending a receipt printing Serial Port (COM) interface for a thermal receipt printer to use a USB interface without needing a Virtual Serial Port. I have a working prototype that will enumerate over the attached USB devices, locate the USB path for a device with a specific vendor id and product id, and open a connection to the device using CreateFile().

The existing Serial Port code uses the Windows API wrapped in a set of functions. The approach I'm taking is to add additional code using the same set of functions but that depend on a USB connection rather than a Serial Port connection. I have previously used the same approach to allow the use of a kitchen printer over either a Serial Port or over a WiFi/LAN connection with minimal changes to existing code successfully.

Unfortunately the existing code that uses the function library depends on the functions to use ReadFile() with a time out specified so that if the thermal printer does not respond to a status request within a reasonable time, the application can mark it as down and allow operations to continue or to use a backup or secondary printer.

How do I specify a time out for a ReadFile() on a file handle from CreateFile() that opens a connection to a communications devices using a USB pathname?

A consideration is this is multi-threaded code used for more than one serial communications device (receipt printer, kitchen printer, scale, etc.) however a thread will have exclusive access to a particular device (kitchen printing functionality opens serial port to kitchen printer only, scale reading functionality opens serial port to scale only, etc.).

In the existing Serial Port code, the function used to set timeouts, 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). This means some other mechanism is needed to provide a way to allow for an I/O failure due to a time out when using a USB device pathname.

We are using the following code segment to open a Serial Port, whether to a hardware COM port or a Virtual Serial Port emulating a hardware COM port:

// 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 ) ;

The ReadFile() is wrapped within a function and looks like:

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);
}

CodePudding user response:

I found a number of similar posted questions which involved pipes however the same approach of using overlapped I/O applies.

I also found the following article on the web Peter's blog: Getting a handle on usbprint.sys which provides code and a description of how to find a USB pathname for a USB connected device. I have used some of that code sample in the class below.

I also found an article on codeproject.com, Enumerating windows device by Chuan-Liang Teng which contained an example of enumerating over the connected USB devices and interrogating various settings and details about the devices. The code from that article, though old, was helpful though not necessary for this particular application.

I have a prototype C class using overlapped I/O which seems to be replicating the behavior seen with a Serial Port connection using a USB connection to a thermal printer. The full source and Visual Studio 2017 solution and project files are in my GitHub repository https://github.com/RichardChambers/utilities_tools/tree/main/UsbWindows as this snip has the most pertinent parts.

I have done a simple test with modified code in the point of sale application and am now in the process of integrating this into the existing thermal receipt printer source code which already works with a Serial Port.

#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;
}
  • Related