公司用了一台发卡机,usb接口,半双工,给了个dll,不支持线程操作,使得UI线程老卡。
懊恼了,想自己直接通过usb读写,各种百度,然后是无数的坑,最终搞定。
现将各种坑和我自己的某些猜想记录一下,也供各位参考。
一、常量定义
1private const short INVALID_HANDLE_VALUE = -1; 2 private const uint GENERIC_READ = 0x80000000; 3 private const uint GENERIC_WRITE = 0x40000000; 4 private const uint FILE_SHARE_READ = 0x00000001; 5 private const uint FILE_SHARE_WRITE = 0x00000002; 6 private const uint CREATE_NEW = 1; 7 private const uint CREATE_ALWAYS = 2; 8 private const uint OPEN_EXISTING = 3; 9 private const uint FILE_FLAG_OVERLAPPED = 0x40000000; 10 private const uint FILE_ATTRIBUTE_NORMAL = 0x00000080;
主要用于CreateFile时用。
二、结构、枚举、类定义
1private struct HID_ATTRIBUTES 2 { 3 public int Size; 4 public ushort VendorID; 5 public ushort ProductID; 6 public ushort VersionNumber; 7 } 8 private struct SP_DEVICE_INTERFACE_DATA 9 { 10 public int cbSize; 11 public Guid interfaceClassGuid; 12 public int flags; 13 public int reserved; 14 } 15 [StructLayout(LayoutKind.Sequential)] 16 private class SP_DEVINFO_DATA 17 { 18 public int cbSize = Marshal.SizeOf<SP_DEVINFO_DATA>(); 19 public Guid classGuid = Guid.Empty; 20 public int devInst = 0; 21 public int reserved = 0; 22 } 23 [StructLayout(LayoutKind.Sequential, Pack = 2)] 24 private struct SP_DEVICE_INTERFACE_DETAIL_DATA 25 { 26 internal int cbSize; 27 internal short devicePath; 28 } 29 private enum DIGCF 30 { 31 DIGCF_DEFAULT = 0x1, 32 DIGCF_PRESENT = 0x2, 33 DIGCF_ALLCLASSES = 0x4, 34 DIGCF_PROFILE = 0x8, 35 DIGCF_DEVICEINTERFACE = 0x10 36 } 37 [StructLayout(LayoutKind.Sequential)] 38 private struct HIDP_CAPS 39 { 40 /// <summary> 41 /// Specifies a top-level collection's usage ID. 42 /// </summary> 43 public System.UInt16 Usage; 44 /// <summary> 45 /// Specifies the top-level collection's usage page. 46 /// </summary> 47 public System.UInt16 UsagePage; 48 /// <summary> 49 /// 输入报告的最大节数数量(如果使用报告ID,则包含报告ID的字节) 50 /// 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). 51 /// </summary> 52 public System.UInt16 InputReportByteLength; 53 /// <summary> 54 /// 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). 55 /// </summary> 56 public System.UInt16 OutputReportByteLength; 57 /// <summary> 58 /// 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). 59 /// </summary> 60 public System.UInt16 FeatureReportByteLength; 61 /// <summary> 62 /// Reserved for internal system use. 63 /// </summary> 64 [MarshalAs(UnmanagedType.ByValArray, SizeConst = 17)] 65 public System.UInt16[] Reserved; 66 /// <summary> 67 /// pecifies the number of HIDP_LINK_COLLECTION_NODE structures that are returned for this top-level collection by HidP_GetLinkCollectionNodes. 68 /// </summary> 69 public System.UInt16 NumberLinkCollectionNodes; 70 /// <summary> 71 /// Specifies the number of input HIDP_BUTTON_CAPS structures that HidP_GetButtonCaps returns. 72 /// </summary> 73 public System.UInt16 NumberInputButtonCaps; 74 /// <summary> 75 /// Specifies the number of input HIDP_VALUE_CAPS structures that HidP_GetValueCaps returns. 76 /// </summary> 77 public System.UInt16 NumberInputValueCaps; 78 /// <summary> 79 /// Specifies the number of data indices assigned to buttons and values in all input reports. 80 /// </summary> 81 public System.UInt16 NumberInputDataIndices; 82 /// <summary> 83 /// Specifies the number of output HIDP_BUTTON_CAPS structures that HidP_GetButtonCaps returns. 84 /// </summary> 85 public System.UInt16 NumberOutputButtonCaps; 86 /// <summary> 87 /// Specifies the number of output HIDP_VALUE_CAPS structures that HidP_GetValueCaps returns. 88 /// </summary> 89 public System.UInt16 NumberOutputValueCaps; 90 /// <summary> 91 /// Specifies the number of data indices assigned to buttons and values in all output reports. 92 /// </summary> 93 public System.UInt16 NumberOutputDataIndices; 94 /// <summary> 95 /// Specifies the total number of feature HIDP_BUTTONS_CAPS structures that HidP_GetButtonCaps returns. 96 /// </summary> 97 public System.UInt16 NumberFeatureButtonCaps; 98 /// <summary> 99 /// Specifies the total number of feature HIDP_VALUE_CAPS structures that HidP_GetValueCaps returns. 100 /// </summary> 101 public System.UInt16 NumberFeatureValueCaps; 102 /// <summary> 103 /// Specifies the number of data indices assigned to buttons and values in all feature reports. 104 /// </summary> 105 public System.UInt16 NumberFeatureDataIndices; 106 }
都是从各种地方复制过来的。最后的结构注释从微软那里复制了英文,翻译了一句中文。因为这个坑最大。
三、Dll封装
1/// <summary> 2 /// 过滤设备,获取需要的设备 3 /// </summary> 4 /// <param name="ClassGuid"></param> 5 /// <param name="Enumerator"></param> 6 /// <param name="HwndParent"></param> 7 /// <param name="Flags"></param> 8 /// <returns></returns> 9 [DllImport("setupapi.dll", SetLastError = true)] 10 private static extern IntPtr SetupDiGetClassDevs(ref Guid ClassGuid, uint Enumerator, IntPtr HwndParent, DIGCF Flags); 11 /// <summary> 12 /// 获取设备,true获取到 13 /// </summary> 14 /// <param name="hDevInfo"></param> 15 /// <param name="devInfo"></param> 16 /// <param name="interfaceClassGuid"></param> 17 /// <param name="memberIndex"></param> 18 /// <param name="deviceInterfaceData"></param> 19 /// <returns></returns> 20 [DllImport("setupapi.dll", CharSet = CharSet.Auto, SetLastError = true)] 21 private static extern Boolean SetupDiEnumDeviceInterfaces(IntPtr hDevInfo, IntPtr devInfo, ref Guid interfaceClassGuid, UInt32 memberIndex, ref SP_DEVICE_INTERFACE_DATA deviceInterfaceData); 22 /// <summary> 23 /// 获取接口的详细信息 必须调用两次 第1次返回长度 第2次获取数据 24 /// </summary> 25 /// <param name="deviceInfoSet"></param> 26 /// <param name="deviceInterfaceData"></param> 27 /// <param name="deviceInterfaceDetailData"></param> 28 /// <param name="deviceInterfaceDetailDataSize"></param> 29 /// <param name="requiredSize"></param> 30 /// <param name="deviceInfoData"></param> 31 /// <returns></returns> 32 [DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Auto)] 33 private static extern bool SetupDiGetDeviceInterfaceDetail(IntPtr deviceInfoSet, ref SP_DEVICE_INTERFACE_DATA deviceInterfaceData, IntPtr deviceInterfaceDetailData, int deviceInterfaceDetailDataSize, ref int requiredSize, SP_DEVINFO_DATA deviceInfoData); 34 /// <summary> 35 /// 删除设备信息并释放内存 36 /// </summary> 37 /// <param name="HIDInfoSet"></param> 38 /// <returns></returns> 39 [DllImport("setupapi.dll", CharSet = CharSet.Auto, SetLastError = true)] 40 private static extern Boolean SetupDiDestroyDeviceInfoList(IntPtr HIDInfoSet); 41 42 43 /// <summary> 44 /// 获取设备文件 45 /// </summary> 46 /// <param name="lpFileName"></param> 47 /// <param name="dwDesiredAccess">access mode</param> 48 /// <param name="dwShareMode">share mode</param> 49 /// <param name="lpSecurityAttributes">SD</param> 50 /// <param name="dwCreationDisposition">how to create</param> 51 /// <param name="dwFlagsAndAttributes">file attributes</param> 52 /// <param name="hTemplateFile">handle to template file</param> 53 /// <returns></returns> 54 [DllImport("kernel32.dll", SetLastError = true)] 55 private static extern IntPtr CreateFile(string lpFileName, uint dwDesiredAccess, uint dwShareMode, uint lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, uint hTemplateFile); 56 [DllImport("kernel32.dll", SetLastError = true)] 57 [return: System.Runtime.InteropServices.MarshalAsAttribute(System.Runtime.InteropServices.UnmanagedType.Bool)] 58 private static extern bool WriteFile(System.IntPtr hFile, byte[] lpBuffer, uint nNumberOfBytesToWrite, out uint lpNumberOfBytesWritten, IntPtr lpOverlapped); 59 [DllImport("kernel32.dll", SetLastError = true)] 60 private static extern int CloseHandle(int hObject); 61 62 /// <summary> 63 /// 获得GUID 64 /// </summary> 65 /// <param name="HidGuid"></param> 66 [DllImport("hid.dll")] 67 private static extern void HidD_GetHidGuid(ref Guid HidGuid); 68 [DllImport("hid.dll")] 69 private static extern Boolean HidD_GetPreparsedData(IntPtr hidDeviceObject, out IntPtr PreparsedData); 70 [DllImport("hid.dll")] 71 private static extern uint HidP_GetCaps(IntPtr PreparsedData, out HIDP_CAPS Capabilities); 72 [DllImport("hid.dll")] 73 private static extern Boolean HidD_FreePreparsedData(IntPtr PreparsedData); 74 [DllImport("hid.dll")] 75 private static extern Boolean HidD_GetAttributes(IntPtr hidDevice, out HID_ATTRIBUTES attributes);
四、几个属性
1private int _InputBufferSize; 2 private int _OutputBufferSize; 3 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设备文件名
1/// <summary> 2 /// 获取所有Usb设备文件名 3 /// </summary> 4 /// <returns></returns> 5 public static List<string> GetUsbFileNames() 6 { 7 List<string> items = new List<string>(); 8 9 //通过一个空的GUID来获取HID的全局GUID。 10 Guid hidGuid = Guid.Empty; 11 HidD_GetHidGuid(ref hidGuid); 12 13 //通过获取到的HID全局GUID来获取包含所有HID接口信息集合的句柄。 14 IntPtr hidInfoSet = SetupDiGetClassDevs(ref hidGuid, 0, IntPtr.Zero, DIGCF.DIGCF_PRESENT | DIGCF.DIGCF_DEVICEINTERFACE); 15 16 //获取接口信息。 17 if (hidInfoSet != IntPtr.Zero) 18 { 19 20 SP_DEVICE_INTERFACE_DATA interfaceInfo = new SP_DEVICE_INTERFACE_DATA(); 21 interfaceInfo.cbSize = Marshal.SizeOf(interfaceInfo); 22 23 uint index = 0; 24 //检测集合的每个接口 25 while (SetupDiEnumDeviceInterfaces(hidInfoSet, IntPtr.Zero, ref hidGuid, index, ref interfaceInfo)) 26 { 27 int bufferSize = 0; 28 //获取接口详细信息;第一次读取错误,但可取得信息缓冲区的大小 29 SP_DEVINFO_DATA strtInterfaceData = new SP_DEVINFO_DATA(); 30 var result = SetupDiGetDeviceInterfaceDetail(hidInfoSet, ref interfaceInfo, IntPtr.Zero, 0, ref bufferSize, null); 31 //第二次调用传递返回值,调用即可成功 32 IntPtr detailDataBuffer = Marshal.AllocHGlobal(bufferSize); 33 Marshal.StructureToPtr( 34 new SP_DEVICE_INTERFACE_DETAIL_DATA 35 { 36 cbSize = Marshal.SizeOf(typeof(SP_DEVICE_INTERFACE_DETAIL_DATA)) 37 }, detailDataBuffer, false); 38 39 40 if (SetupDiGetDeviceInterfaceDetail(hidInfoSet, ref interfaceInfo, detailDataBuffer, bufferSize, ref bufferSize, null))// strtInterfaceData)) 41 { 42 string devicePath = Marshal.PtrToStringAuto(IntPtr.Add(detailDataBuffer, 4)); 43 items.Add(devicePath); 44 } 45 Marshal.FreeHGlobal(detailDataBuffer); 46 index++; 47 } 48 } 49 //删除设备信息并释放内存 50 SetupDiDestroyDeviceInfoList(hidInfoSet); 51 return items; 52 }
一般会返回好几个文件名,那哪个是你要的呢?方法有二:
1.先获取一次文件名列表,然后插拔或者禁用启用一次Usb设备,变化的那个就是
2.轮流写然后读一次文件名,获取到正确结果的就是
我采用2,然后User.config里面把他记下来。
要读写,首先要打开
(二)打开Usb设备
1/// <summary> 2 /// 构造 3 /// </summary> 4 /// <param name="usbFileName">Usb Device Path</param> 5 public UsbApi(string usbFileName) 6 { 7 if (string.IsNullOrEmpty(usbFileName)) 8 throw new Exception("文件名不能为空"); 9 10 var fileHandle = CreateFile( 11 usbFileName, 12 GENERIC_READ | GENERIC_WRITE,// | GENERIC_WRITE,//读写,或者一起 13 FILE_SHARE_READ | FILE_SHARE_WRITE,//共享读写,或者一起 14 0, 15 OPEN_EXISTING,//必须已经存在 16 FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, 17 0); 18 if (fileHandle == IntPtr.Zero || (int)fileHandle == -1) 19 throw new Exception("打开文件失败"); 20 21 HidD_GetAttributes(fileHandle, out var attributes);// null);// out var aa); 22 HidD_GetPreparsedData(fileHandle, out var preparseData); 23 HidP_GetCaps(preparseData, out var caps); 24 HidD_FreePreparsedData(preparseData); 25 _InputBufferSize = caps.InputReportByteLength; 26 _OutputBufferSize = caps.OutputReportByteLength; 27 28 _UsbFileStream = new FileStream(new SafeFileHandle(fileHandle, true), FileAccess.ReadWrite, System.Math.Max(caps.OutputReportByteLength, caps.InputReportByteLength), true); 29 }
打开Usb设备我是在构找函数里面完成的,我的类名叫UsbApi。
(三)写
1/// <summary> 2 /// 写数据 3 /// </summary> 4 /// <param name="array"></param> 5 public void Write(byte[] data) 6 { 7 if (_UsbFileStream == null) 8 throw new Exception("Usb设备没有初始化"); 9 if (data.Length > _OutputBufferSize) 10 throw new Exception($"数据太长,超出缓冲区长度({_OutputBufferSize})"); 11 byte[] outBuffer = new byte[_OutputBufferSize]; 12 Array.Copy(data, 0, outBuffer, 1, data.Length); 13 _UsbFileStream.Write(outBuffer, 0, _OutputBufferSize); 14 }
(四)读
1/// <summary> 2 /// 同步读 3 /// </summary> 4 /// <param name="array"></param> 5 public byte[] Read() 6 {
if (_UsbFileStream == null)
throw new Exception("Usb设备没有初始化");
1 byte[] inBuffer = new byte[_InputBufferSize]; 2 _UsbFileStream.Read(inBuffer, 0, _InputBufferSize); 3 return inBuffer; 4 }
我的Usb设备是半双工的,并且数据只有64字节,所有用了同步读。
(五)关闭
1public void Close() 2 { 3 if (_UsbFileStream != null) 4 _UsbFileStream.Close(); 5 }
六、最后
最后写了几行代码测试,巨坑:
1.CreateFile参数的坑
1 var fileHandle = CreateFile( 2 usbFileName, 3 GENERIC_READ | GENERIC_WRITE,// | GENERIC_WRITE,//读写,或者一起 4 FILE_SHARE_READ | FILE_SHARE_WRITE,//共享读写,或者一起 5 0, 6 OPEN_EXISTING,//必须已经存在 7 FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, 8 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的巨坑
1public void Write(byte[] data)这个data长度必须与缓冲区大写一样,而且数据要从data[1]开始写,如你要写“AB”,var data=new byte[]{0,(byte)'A',(byte)'B'}; 2 3事后发现HIDP_CAPS里面的某个值可能告诉我了。趟了这些坑后,搞定了,能用线程了^-^,发文纪念。