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 = ;
public int reserved = ;
}
[StructLayout(LayoutKind.Sequential, Pack = )]
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 = )]
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, , 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 = ;
//检测集合的每个接口
while (SetupDiEnumDeviceInterfaces(hidInfoSet, IntPtr.Zero, ref hidGuid, index, ref interfaceInfo))
{
int bufferSize = ;
//获取接口详细信息;第一次读取错误,但可取得信息缓冲区的大小
SP_DEVINFO_DATA strtInterfaceData = new SP_DEVINFO_DATA();
var result = SetupDiGetDeviceInterfaceDetail(hidInfoSet, ref interfaceInfo, IntPtr.Zero, , 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, ));
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,//共享读写,或者一起
,
OPEN_EXISTING,//必须已经存在
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
);
if (fileHandle == IntPtr.Zero || (int)fileHandle == -)
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, , outBuffer, , data.Length);
_UsbFileStream.Write(outBuffer, , _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, , _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下位机读写的更多相关文章
- 带你十天轻松搞定 Go 微服务系列(一)
本文开始,我们会出一个系列文章跟大家详细展示一个 go-zero 微服务示例,整个系列分十篇文章,目录结构如下: 环境搭建(本文) 服务拆分 用户服务 产品服务 订单服务 支付服务 RPC 服务 Au ...
- 带你十天轻松搞定 Go 微服务系列(二)
上篇文章开始,我们通过一个系列文章跟大家详细展示一个 go-zero 微服务示例,整个系列分十篇文章,目录结构如下: 环境搭建 服务拆分(本文) 用户服务 产品服务 订单服务 支付服务 RPC 服务 ...
- 带你十天轻松搞定 Go 微服务系列(三)
序言 我们通过一个系列文章跟大家详细展示一个 go-zero 微服务示例,整个系列分十篇文章,目录结构如下: 环境搭建 服务拆分 用户服务(本文) 产品服务 订单服务 支付服务 RPC 服务 Auth ...
- 带你十天轻松搞定 Go 微服务系列(五)
序言 我们通过一个系列文章跟大家详细展示一个 go-zero 微服务示例,整个系列分十篇文章,目录结构如下: 环境搭建 服务拆分 用户服务 产品服务 订单服务(本文) 支付服务 RPC 服务 Auth ...
- 带你十天轻松搞定 Go 微服务系列(六)
序言 我们通过一个系列文章跟大家详细展示一个 go-zero 微服务示例,整个系列分十篇文章,目录结构如下: 环境搭建 服务拆分 用户服务 产品服务 订单服务 支付服务(本文) RPC 服务 Auth ...
- 带你十天轻松搞定 Go 微服务系列(七)
序言 我们通过一个系列文章跟大家详细展示一个 go-zero 微服务示例,整个系列分十篇文章,目录结构如下: 环境搭建 服务拆分 用户服务 产品服务 订单服务 支付服务 RPC 服务 Auth 验证( ...
- 带你十天轻松搞定 Go 微服务系列(八、服务监控)
序言 我们通过一个系列文章跟大家详细展示一个 go-zero 微服务示例,整个系列分十篇文章,目录结构如下: 环境搭建 服务拆分 用户服务 产品服务 订单服务 支付服务 RPC 服务 Auth 验证 ...
- 带你十天轻松搞定 Go 微服务系列(九、链路追踪)
序言 我们通过一个系列文章跟大家详细展示一个 go-zero 微服务示例,整个系列分十篇文章,目录结构如下: 环境搭建 服务拆分 用户服务 产品服务 订单服务 支付服务 RPC 服务 Auth 验证 ...
- 带你十天轻松搞定 Go 微服务之大结局(分布式事务)
序言 我们通过一个系列文章跟大家详细展示一个 go-zero 微服务示例,整个系列分十篇文章,目录结构如下: 环境搭建 服务拆分 用户服务 产品服务 订单服务 支付服务 RPC 服务 Auth 验证 ...
随机推荐
- Descriptio Resource Path LocationType Archive for required library: 'D:/apache-maven/apache-maven-3.6.0/mavenrepository/org/springframework/spring-aspects/4.3.7.RELEASE/spring-aspects-4.3.7.RELEASE.
eclipse创建了一个maven项目后,在导入依赖包后返现项目有个红色刺眼的感叹号,再看控制台有个Problem提示 Descriptio Resource Path LocationType Ar ...
- LRU算法的Java实现
LRU全称是Least Recently Used,即最近最久未使用的意思. LRU算法的设计原则是:如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小.也就是说,当限定的空间已 ...
- python实现汉诺塔问题
汉诺塔问题可以简单描述成为将a柱子上的圆盘按一定规则借助b柱子完美地复制到c柱子上.现假设有a,b,c三根柱子,a柱子上的圆盘从上到下依次标号为1,2,3,……,n,且为递增状态.规则:每次移动一个盘 ...
- line-height:150%与line-height:1.5的区别
今天看到一篇文章,说的是CSS学习中的瓶颈,我最近也发现自己css很薄弱,写的样式总是有兼容性问题,要写很久,发现了一个问题,我从来没有用过line-height:150和line-height:1. ...
- python--个人信息修改程序
创建一个新的文本,account.txt,输入以下个人信息内容, lanyinhua,lanyinhua,蓝银花,22,Model,PR,22alex,123,华仔 Li,222,CEO,IT,133 ...
- h5软键盘弹起 底部按钮被顶起问题解决
解决思路: 当键盘弹起时隐藏掉按钮,键盘隐藏时按钮显示 监测键盘是否弹起(浏览器页面是否发生变化) 代码: 1.定义一个底部按钮 <div class="returnbtn" ...
- c# Winform Chart入门
额外参考链接:http://www.cnblogs.com/greenerycn/archive/2008/10/27/microsoft-chart.html winform 仪表盘相关下载链接:/ ...
- 八大排序算法详解(动图演示 思路分析 实例代码java 复杂度分析 适用场景)
一.分类 1.内部排序和外部排序 内部排序:待排序记录存放在计算机随机存储器中(说简单点,就是内存)进行的排序过程. 外部排序:待排序记录的数量很大,以致于内存不能一次容纳全部记录,所以在排序过程中需 ...
- There are multiple modules with names that only differ in casing. 黄色warning
There are multiple modules with names that only differ in casing.有多个模块同名仅大小写不同This can lead to unexp ...
- AS3.0 给addEventListener里的方法加上参数传递
方法一:for(var i:int=1;i<=4;i++){this["btn"+i].addEventListener(MouseEvent.CLICK,EventUp(b ...