I have programmed a microcontroller with USB and connected it a PC using WinUSB. I am able to communicate with my uC in c++ and in VB.NET using pinvoke with synchronous (blocking, overlapped = NULL) calls. I just got asynchronous calls working in c++ but I don't know how to get it coded in .NET, there is also another question I am not sure of:
Question 1: In c++ I initially tried asynchronous calls using IO Completion Callback Objects but when I called WinUSB to read it returned an error that asynchronous IO was already in progress on the file handle (HANDLE hDevice), CreateThreadpoolIo would not take the WinUSB handle (PWINUSB_INTERFACE_HANDLE hWinUsbHandle). My guess is WinUSB uses this file handle for IO and so I cannot. I eventually got it working using Wait Callback Objects. It is working now, I just want to clarify that what I am doing is correct usage.
The part that I was a little confused about is the CreateThreadpoolWait call. At first I thought this was creating a thread pool like other examples from MSDN, however I think now it is just an object that runs on a default thread pool. Also, objConext is the variable to use to sync calls with their callbacks, that is PVOID Context in the callback points to objConext.
Here is the c code snippet:
// Receive data asynchronously
void BeginReceiveData(UCHAR bytPipeId, UCHAR *bytData, ULONG intLength, PTP_WAIT_CALLBACK cbCallback)
{
// Create the event
HANDLE hEventCallback = CreateEvent(NULL, FALSE, FALSE, NULL);
// Check for an error
if (!hEventCallback)
{
// Set the error
printf(strError, "Error creating callback event: %d.", GetLastError());
}
// Create the thread pool wait object
tpWait = CreateThreadpoolWait(cbCallback, &objConext, NULL);
// Check for an error
if (!tpWait)
{
// Set the error
printf(strError, "Error creating thread pool: %d.", GetLastError());
}
// Place the wait object in the thread pool
SetThreadpoolWait(tpWait, hEventCallback, NULL);
// Clear the callback
ZeroMemory(&oCallback, sizeof(oCallback));
// Set the event
oCallback.hEvent = hEventCallback;
// Read
BOOL bResult = WinUsb_ReadPipe(*hWinUsbHandle, bytPipeId, bytData, intLength, NULL, &oCallback);
// Check for an error
if (!bResult)
{
if (GetLastError() != ERROR_IO_PENDING)
{
// Set the error
printf(strError, "Error reading pipe: %d.", GetLastError());
}
}
}
// Receive data callback
void CALLBACK UsbCallback(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_WAIT Wait, TP_WAIT_RESULT WaitResult)
{
}
Question 2: How to convert the above to .NET? This is my guess so far:
Dim wait As New WaitCallback(AddressOf UsbCallback)
' Create the event
Dim eEventCallback As New AutoResetEvent(False)
ThreadPool.QueueUserWorkItem(wait, Nothing)
Dim oCallback As New Overlapped(0, 0, eEventCallback.SafeWaitHandle.DangerousGetHandle, Nothing)
'Dim oCallback As New NativeOverlapped
oCallback.EventHandleIntPtr = eEventCallback.SafeWaitHandle.DangerousGetHandle
' Read
Dim bResult As Boolean = WinUsb_ReadPipe(hWinUsbHandle, bytPipeId, bytData, intLength, intLength, oCallback.EventHandleIntPtr)
<DllImport("winusb.dll", SetLastError:=True, CharSet:=CharSet.Auto)>
Public Function WinUsb_ReadPipe(InterfaceHandle As IntPtr, PipeID As Byte, Buffer() As Byte, BufferLength As UInteger, ByRef LengthTransferred As UInteger, Overlapped As IntPtr) As Boolean
End Function
Edit: I was going to post as an answer but I don't have enough street cred
Well, I sort of have the answer to convert the code to .NET. I managed to convert all API functions to PInvoke and then replaced the ones that were built into .NET except a couple. The code works but care must be taken to where the variables are declared as GC will clean them up before the callback is made. Also, the context must be carefully allocated and destroyed. For example an array in the context structure may be cleaned if the right size of memory is not allocated, which is easy to understand.
For these reasons it may be more beneficial to keep handle references in an object for every pipe in USB and reuse them since multiple calls cannot be made to the same pipe before the previous one completes. If that is done then you can do the same with the context and avoid allocating and destroying memory for the context. for example, a dictionary can be used on the Wait pointer in the callback:
Dim dicHandlesAndContext As Dictionary(Of IntPtr, HandlesAndContext)
Although I haven't really found an answer to question 1: whether or not this is the 'right' way to do it, it seems to perform quite well. Here are the main code snippets:
' Receive data asynchronously
Private Sub BeginReceiveData(bytPipeId As Byte, bytData() As Byte, intLength As UInteger)
' Allocate memory for the callback
ptrContext = Marshal.AllocHGlobal(Marshal.SizeOf(GetType(TestContext)))
' Copy the structure to memory
Marshal.StructureToPtr(objConext, ptrContext, True)
' Create the callback
cbCallback = New UsbCallbackDelegate(AddressOf UsbCallback)
' Create the event
Dim areEvent As New AutoResetEvent(False)
' Set the event
hEventCallback = areEvent.SafeWaitHandle.DangerousGetHandle
' Create the thread pool wait object
tpWait = CreateThreadpoolWait(Marshal.GetFunctionPointerForDelegate(cbCallback), ptrContext, IntPtr.Zero)
' Place the wait object in the thread pool
SetThreadpoolWait(tpWait, hEventCallback, IntPtr.Zero)
' Set the event
oCallback.EventHandle = hEventCallback
' Read
Dim bResult As Boolean = WinUsb_ReadPipe(hWinUsbHandle, bytPipeId, bytData, intLength, intLength, oCallback)
If Not bResult Then
If Not Marshal.GetLastWin32Error = 997 Then
' Get the error
Dim intError As Integer = Marshal.GetLastWin32Error
Throw New Win32Exception(intError, "Error getting device information.")
End If
End If
End Sub
' Delegate for USB callbacks
Private Delegate Sub UsbCallbackDelegate(Instance As IntPtr, Context As IntPtr, Wait As IntPtr, WaitResult As UInteger)
' Callback
Private Sub UsbCallback(Instance As IntPtr, Context As IntPtr, Wait As IntPtr, WaitResult As UInteger)
' Number of bytes transferred
Dim intTransferred As UInteger
' Get the number of bytes transferred
WinUsb_GetOverlappedResult(hWinUsbHandle, oCallback, intTransferred, False)
' Get the context from memory
Dim objConext As TestContext = Marshal.PtrToStructure(Context, GetType(TestContext))
' Free the memory
Marshal.FreeHGlobal(Context)
' Do some work on the data
End Sub
The main API DLL imports
<DllImport("winusb.dll", SetLastError:=True, CharSet:=CharSet.Auto)>
Public Function WinUsb_GetOverlappedResult(InterfaceHandle As IntPtr, ByRef lpOverlapped As Threading.NativeOverlapped, ByRef lpNumberOfBytesTransferred As UInteger, bWait As Boolean) As IntPtr
End Function
<DllImport("kernel32.dll", SetLastError:=True, CharSet:=CharSet.Auto)>
Public Function CreateThreadpoolWait(pfnwa As IntPtr, pv As IntPtr, pcbe As IntPtr) As IntPtr
End Function
<DllImport("kernel32.dll", SetLastError:=True, CharSet:=CharSet.Auto)>
Public Function SetThreadpoolWait(pwa As IntPtr, h As IntPtr, pftTimeout As IntPtr) As IntPtr
End Function
<DllImport("kernel32.dll", SetLastError:=True, CharSet:=CharSet.Auto)>
Public Function CloseThreadpoolWait(pwa As IntPtr) As IntPtr
End Function
I tested these functions for GC protection by receiving data continuously and running GC at the same time, the program will crash if something was collected.
' Receive data asynchronously on endpoint 1
BeginReceiveData(129, bytData, 8)
While True
' Garbage collection
GC.Collect()
GC.WaitForFullGCComplete()
End While
' Callback
Private Sub UsbCallback(Instance As IntPtr, Context As IntPtr, Wait As IntPtr, WaitResult As UInteger)
' ...
' Once data is received, receive more
BeginReceiveData(129, bytData, 8)
'...
End Sub
WARNING: A lot of cleanup code is not here, check MSDN for c code on how to do this properly. I think an object oriented approach is probably best to keep handles alive and implement IDIsposable to clean up when done.
Anyone interested in an Open Source DLL? :P