C# 連蒙帶騙不知所以然的搞定USB下位機讀寫
公司用了一臺發卡機,usb接口,半雙工,給了個dll,不支持線程操作,使得UI線程老卡。
懊惱了,想自己直接通過usb讀寫,各種百度,然後是無數的坑,最終搞定。
現將各種坑和我自己的某些猜想記錄一下,也供各位參考。
一、常量定義
private const short INVALID_HANDLE_VALUE = -1; private const uint GENERIC_READ = 0x80000000; private const uint GENERIC_WRITE = 0x40000000; private const uint FILE_SHARE_READ = 0x00000001; private const uint FILE_SHARE_WRITE = 0x00000002; private const uint CREATE_NEW = 1; private const uint CREATE_ALWAYS = 2; private const uint OPEN_EXISTING = 3; private const uint FILE_FLAG_OVERLAPPED = 0x40000000; private const uint FILE_ATTRIBUTE_NORMAL = 0x00000080;
主要用於CreateFile時用。
二、結構、枚舉、類定義
private struct HID_ATTRIBUTES { public int Size; public ushort VendorID; public ushort ProductID; public ushort VersionNumber; } private struct SP_DEVICE_INTERFACE_DATA {public int cbSize; public Guid interfaceClassGuid; public int flags; public int reserved; } [StructLayout(LayoutKind.Sequential)] private class SP_DEVINFO_DATA { public int cbSize = Marshal.SizeOf<SP_DEVINFO_DATA>();public Guid classGuid = Guid.Empty; public int devInst = 0; public int reserved = 0; } [StructLayout(LayoutKind.Sequential, Pack = 2)] private struct SP_DEVICE_INTERFACE_DETAIL_DATA { internal int cbSize; internal short devicePath; } private enum DIGCF { DIGCF_DEFAULT = 0x1, DIGCF_PRESENT = 0x2, DIGCF_ALLCLASSES = 0x4, DIGCF_PROFILE = 0x8, DIGCF_DEVICEINTERFACE = 0x10 } [StructLayout(LayoutKind.Sequential)] private struct HIDP_CAPS { /// <summary> /// Specifies a top-level collection‘s usage ID. /// </summary> public System.UInt16 Usage; /// <summary> /// Specifies the top-level collection‘s usage page. /// </summary> public System.UInt16 UsagePage; /// <summary> /// 輸入報告的最大節數數量(如果使用報告ID,則包含報告ID的字節) /// Specifies the maximum size, in bytes, of all the input reports (including the report ID, if report IDs are used, which is prepended to the report data). /// </summary> public System.UInt16 InputReportByteLength; /// <summary> /// Specifies the maximum size, in bytes, of all the output reports (including the report ID, if report IDs are used, which is prepended to the report data). /// </summary> public System.UInt16 OutputReportByteLength; /// <summary> /// Specifies the maximum length, in bytes, of all the feature reports (including the report ID, if report IDs are used, which is prepended to the report data). /// </summary> public System.UInt16 FeatureReportByteLength; /// <summary> /// Reserved for internal system use. /// </summary> [MarshalAs(UnmanagedType.ByValArray, SizeConst = 17)] public System.UInt16[] Reserved; /// <summary> /// pecifies the number of HIDP_LINK_COLLECTION_NODE structures that are returned for this top-level collection by HidP_GetLinkCollectionNodes. /// </summary> public System.UInt16 NumberLinkCollectionNodes; /// <summary> /// Specifies the number of input HIDP_BUTTON_CAPS structures that HidP_GetButtonCaps returns. /// </summary> public System.UInt16 NumberInputButtonCaps; /// <summary> /// Specifies the number of input HIDP_VALUE_CAPS structures that HidP_GetValueCaps returns. /// </summary> public System.UInt16 NumberInputValueCaps; /// <summary> /// Specifies the number of data indices assigned to buttons and values in all input reports. /// </summary> public System.UInt16 NumberInputDataIndices; /// <summary> /// Specifies the number of output HIDP_BUTTON_CAPS structures that HidP_GetButtonCaps returns. /// </summary> public System.UInt16 NumberOutputButtonCaps; /// <summary> /// Specifies the number of output HIDP_VALUE_CAPS structures that HidP_GetValueCaps returns. /// </summary> public System.UInt16 NumberOutputValueCaps; /// <summary> /// Specifies the number of data indices assigned to buttons and values in all output reports. /// </summary> public System.UInt16 NumberOutputDataIndices; /// <summary> /// Specifies the total number of feature HIDP_BUTTONS_CAPS structures that HidP_GetButtonCaps returns. /// </summary> public System.UInt16 NumberFeatureButtonCaps; /// <summary> /// Specifies the total number of feature HIDP_VALUE_CAPS structures that HidP_GetValueCaps returns. /// </summary> public System.UInt16 NumberFeatureValueCaps; /// <summary> /// Specifies the number of data indices assigned to buttons and values in all feature reports. /// </summary> public System.UInt16 NumberFeatureDataIndices; }
都是從各種地方復制過來的。最後的結構註釋從微軟那裏復制了英文,翻譯了一句中文。因為這個坑最大。
三、Dll封裝
/// <summary> /// 過濾設備,獲取需要的設備 /// </summary> /// <param name="ClassGuid"></param> /// <param name="Enumerator"></param> /// <param name="HwndParent"></param> /// <param name="Flags"></param> /// <returns></returns> [DllImport("setupapi.dll", SetLastError = true)] private static extern IntPtr SetupDiGetClassDevs(ref Guid ClassGuid, uint Enumerator, IntPtr HwndParent, DIGCF Flags); /// <summary> /// 獲取設備,true獲取到 /// </summary> /// <param name="hDevInfo"></param> /// <param name="devInfo"></param> /// <param name="interfaceClassGuid"></param> /// <param name="memberIndex"></param> /// <param name="deviceInterfaceData"></param> /// <returns></returns> [DllImport("setupapi.dll", CharSet = CharSet.Auto, SetLastError = true)] private static extern Boolean SetupDiEnumDeviceInterfaces(IntPtr hDevInfo, IntPtr devInfo, ref Guid interfaceClassGuid, UInt32 memberIndex, ref SP_DEVICE_INTERFACE_DATA deviceInterfaceData); /// <summary> /// 獲取接口的詳細信息 必須調用兩次 第1次返回長度 第2次獲取數據 /// </summary> /// <param name="deviceInfoSet"></param> /// <param name="deviceInterfaceData"></param> /// <param name="deviceInterfaceDetailData"></param> /// <param name="deviceInterfaceDetailDataSize"></param> /// <param name="requiredSize"></param> /// <param name="deviceInfoData"></param> /// <returns></returns> [DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Auto)] private static extern bool SetupDiGetDeviceInterfaceDetail(IntPtr deviceInfoSet, ref SP_DEVICE_INTERFACE_DATA deviceInterfaceData, IntPtr deviceInterfaceDetailData, int deviceInterfaceDetailDataSize, ref int requiredSize, SP_DEVINFO_DATA deviceInfoData); /// <summary> /// 刪除設備信息並釋放內存 /// </summary> /// <param name="HIDInfoSet"></param> /// <returns></returns> [DllImport("setupapi.dll", CharSet = CharSet.Auto, SetLastError = true)] private static extern Boolean SetupDiDestroyDeviceInfoList(IntPtr HIDInfoSet); /// <summary> /// 獲取設備文件 /// </summary> /// <param name="lpFileName"></param> /// <param name="dwDesiredAccess">access mode</param> /// <param name="dwShareMode">share mode</param> /// <param name="lpSecurityAttributes">SD</param> /// <param name="dwCreationDisposition">how to create</param> /// <param name="dwFlagsAndAttributes">file attributes</param> /// <param name="hTemplateFile">handle to template file</param> /// <returns></returns> [DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr CreateFile(string lpFileName, uint dwDesiredAccess, uint dwShareMode, uint lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, uint hTemplateFile); [DllImport("kernel32.dll", SetLastError = true)] [return: System.Runtime.InteropServices.MarshalAsAttribute(System.Runtime.InteropServices.UnmanagedType.Bool)] private static extern bool WriteFile(System.IntPtr hFile, byte[] lpBuffer, uint nNumberOfBytesToWrite, out uint lpNumberOfBytesWritten, IntPtr lpOverlapped); [DllImport("kernel32.dll", SetLastError = true)] private static extern int CloseHandle(int hObject); /// <summary> /// 獲得GUID /// </summary> /// <param name="HidGuid"></param> [DllImport("hid.dll")] private static extern void HidD_GetHidGuid(ref Guid HidGuid); [DllImport("hid.dll")] private static extern Boolean HidD_GetPreparsedData(IntPtr hidDeviceObject, out IntPtr PreparsedData); [DllImport("hid.dll")] private static extern uint HidP_GetCaps(IntPtr PreparsedData, out HIDP_CAPS Capabilities); [DllImport("hid.dll")] private static extern Boolean HidD_FreePreparsedData(IntPtr PreparsedData); [DllImport("hid.dll")] private static extern Boolean HidD_GetAttributes(IntPtr hidDevice, out HID_ATTRIBUTES attributes);
四、幾個屬性
private int _InputBufferSize; private int _OutputBufferSize; private FileStream _UsbFileStream = null;
五、幾個方法
Usb設備的讀寫跟磁盤文件的讀寫沒區別,需要打開文件、讀文件、寫文件,最後關閉文件。
磁盤文件大家都清楚,比如“c:\data\hello.txt”就是個文件名,前面加“\\計算機A”則是其他計算機上的某文件名,Usb設備也有文件名,如我的設備的文件名就是“\\?\hid#vid_5131&pid_2007#7&252e9bc9&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}”。
哪弄來的?頭大了吧,我也頭大,什麽鬼?百度了,未果,所以幹脆不管了。先來一個列出全部Usb設備文件名的方法
(一)獲取所有Usb設備文件名
/// <summary> /// 獲取所有Usb設備文件名 /// </summary> /// <returns></returns> public static List<string> GetUsbFileNames() { List<string> items = new List<string>(); //通過一個空的GUID來獲取HID的全局GUID。 Guid hidGuid = Guid.Empty; HidD_GetHidGuid(ref hidGuid); //通過獲取到的HID全局GUID來獲取包含所有HID接口信息集合的句柄。 IntPtr hidInfoSet = SetupDiGetClassDevs(ref hidGuid, 0, IntPtr.Zero, DIGCF.DIGCF_PRESENT | DIGCF.DIGCF_DEVICEINTERFACE); //獲取接口信息。 if (hidInfoSet != IntPtr.Zero) { SP_DEVICE_INTERFACE_DATA interfaceInfo = new SP_DEVICE_INTERFACE_DATA(); interfaceInfo.cbSize = Marshal.SizeOf(interfaceInfo); uint index = 0; //檢測集合的每個接口 while (SetupDiEnumDeviceInterfaces(hidInfoSet, IntPtr.Zero, ref hidGuid, index, ref interfaceInfo)) { int bufferSize = 0; //獲取接口詳細信息;第一次讀取錯誤,但可取得信息緩沖區的大小 SP_DEVINFO_DATA strtInterfaceData = new SP_DEVINFO_DATA(); var result = SetupDiGetDeviceInterfaceDetail(hidInfoSet, ref interfaceInfo, IntPtr.Zero, 0, ref bufferSize, null); //第二次調用傳遞返回值,調用即可成功 IntPtr detailDataBuffer = Marshal.AllocHGlobal(bufferSize); Marshal.StructureToPtr( new SP_DEVICE_INTERFACE_DETAIL_DATA { cbSize = Marshal.SizeOf(typeof(SP_DEVICE_INTERFACE_DETAIL_DATA)) }, detailDataBuffer, false); if (SetupDiGetDeviceInterfaceDetail(hidInfoSet, ref interfaceInfo, detailDataBuffer, bufferSize, ref bufferSize, null))// strtInterfaceData)) { string devicePath = Marshal.PtrToStringAuto(IntPtr.Add(detailDataBuffer, 4)); items.Add(devicePath); } Marshal.FreeHGlobal(detailDataBuffer); index++; } } //刪除設備信息並釋放內存 SetupDiDestroyDeviceInfoList(hidInfoSet); return items; }
一般會返回好幾個文件名,那哪個是你要的呢?方法有二:
1.先獲取一次文件名列表,然後插拔或者禁用啟用一次Usb設備,變化的那個就是
2.輪流寫然後讀一次文件名,獲取到正確結果的就是
我采用2,然後User.config裏面把他記下來。
要讀寫,首先要打開
(二)打開Usb設備
/// <summary> /// 構造 /// </summary> /// <param name="usbFileName">Usb Device Path</param> public UsbApi(string usbFileName) { if (string.IsNullOrEmpty(usbFileName)) throw new Exception("文件名不能為空"); var fileHandle = CreateFile( usbFileName, GENERIC_READ | GENERIC_WRITE,// | GENERIC_WRITE,//讀寫,或者一起 FILE_SHARE_READ | FILE_SHARE_WRITE,//共享讀寫,或者一起 0, OPEN_EXISTING,//必須已經存在 FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, 0); if (fileHandle == IntPtr.Zero || (int)fileHandle == -1) throw new Exception("打開文件失敗"); HidD_GetAttributes(fileHandle, out var attributes);// null);// out var aa); HidD_GetPreparsedData(fileHandle, out var preparseData); HidP_GetCaps(preparseData, out var caps); HidD_FreePreparsedData(preparseData); _InputBufferSize = caps.InputReportByteLength; _OutputBufferSize = caps.OutputReportByteLength; _UsbFileStream = new FileStream(new SafeFileHandle(fileHandle, true), FileAccess.ReadWrite, System.Math.Max(caps.OutputReportByteLength, caps.InputReportByteLength), true); }
打開Usb設備我是在構找函數裏面完成的,我的類名叫UsbApi。
(三)寫
/// <summary> /// 寫數據 /// </summary> /// <param name="array"></param> public void Write(byte[] data) { if (_UsbFileStream == null) throw new Exception("Usb設備沒有初始化"); if (data.Length > _OutputBufferSize) throw new Exception($"數據太長,超出緩沖區長度({_OutputBufferSize})"); byte[] outBuffer = new byte[_OutputBufferSize]; Array.Copy(data, 0, outBuffer, 1, data.Length); _UsbFileStream.Write(outBuffer, 0, _OutputBufferSize); }
(四)讀
/// <summary> /// 同步讀 /// </summary> /// <param name="array"></param> public byte[] Read() {
if (_UsbFileStream == null)
throw new Exception("Usb設備沒有初始化");
byte[] inBuffer = new byte[_InputBufferSize]; _UsbFileStream.Read(inBuffer, 0, _InputBufferSize); return inBuffer; }
我的Usb設備是半雙工的,並且數據只有64字節,所有用了同步讀。
(五)關閉
public void Close() { if (_UsbFileStream != null) _UsbFileStream.Close(); }
六、最後
最後寫了幾行代碼測試,巨坑:
1.CreateFile參數的坑
var fileHandle = CreateFile(
usbFileName,
GENERIC_READ | GENERIC_WRITE,// | GENERIC_WRITE,//讀寫,或者一起
FILE_SHARE_READ | FILE_SHARE_WRITE,//共享讀寫,或者一起
0,
OPEN_EXISTING,//必須已經存在
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
0);
這些參數是針對我的Usb設備,各種調整後達到了能讀寫、能異步。
2.FileStream參數的坑
_UsbFileStream = new FileStream(new SafeFileHandle(fileHandle, true), FileAccess.ReadWrite, System.Math.Max(caps.OutputReportByteLength, caps.InputReportByteLength), true);
緩沖區大小最終采用 System.Math.Max(caps.OutputReportByteLength, caps.InputReportByteLength)
太小讀寫錯誤,大點似乎沒關系
3.Write的巨坑
public void Write(byte[] data)
這個data長度必須與緩沖區大寫一樣,而且數據要從data[1]開始寫,如你要寫“AB”,
var data=new byte[]{0,(byte)‘A‘,(byte)‘B‘};
事後發現HIDP_CAPS裏面的某個值可能告訴我了。
趟了這些坑後,搞定了,能用線程了^-^,發文紀念。
C# 連蒙帶騙不知所以然的搞定USB下位機讀寫