C# 连蒙带骗不知所以然的搞定USB下位机读写

简介: 原文:C# 连蒙带骗不知所以然的搞定USB下位机读写公司用了一台发卡机,usb接口,半双工,给了个dll,不支持线程操作,使得UI线程老卡。 懊恼了,想自己直接通过usb读写,各种百度,然后是无数的坑,最终搞定。
原文: 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里面的某个值可能告诉我了。

趟了这些坑后,搞定了,能用线程了^-^,发文纪念。
目录
相关文章
|
6月前
|
C#
【C#】C#读写Excel文件
【C#】C#读写Excel文件
143 1
|
7月前
|
安全 API C#
C# | System.IO.Pipelines 很酷的读写数据流方式!
文本分享一种新的读写数据流方式 —— System.IO.Pipelines。这个东西在 .NET Core 2.1 中出现了,它能够帮助你更高效地处理数据流。 System.IO.Pipelines 是啥? System.IO.Pipelines 是一个用于读写数据流的高性能 API。它主要由三个部分组成:Pipe、PipelineReader 和 PipelineWriter。 Pipe 是一个异步、线程安全的缓冲区,它让数据在生产者和消费者之间流动。PipelineReader 和 PipelineWriter 则是 Pipe 的读取和写入端点。
369 0
C# | System.IO.Pipelines 很酷的读写数据流方式!
|
7月前
|
存储 Java C#
C# | 使用Memory<T>高效地读写内存数据
Memory<T>是什么? 它是一种可变大小、可读写的内存块,可以安全地暴露给用户代码进行操作。 为什么要使用Memory<T>? 使用它有许多好处。最主要的是善用它可以提高代码的性能。因为Memory<T>是可变的,所以我们可以直接在内存中操作数据,而不需要进行额外的拷贝操作。 使用Memory<T>还可以减少垃圾回收的压力,因为我们不需要创建新的对象来存储数据。 Memory<T>还可以与Span<T>和ReadOnlySpan<T>类型一起使用,这些类型可以方便地对数据进行访问和操作。
130 0
|
C#
C#之四十二 C# 读写文本文件乱码解决方案
C#之四十二 C# 读写文本文件乱码解决方案
411 0
|
JSON C# 数据格式
C#编程-101:读写Json文件示例
C#编程-101:读写Json文件示例
230 0
C#编程-28:读写注册表
C#编程-28:读写注册表
147 0
|
C# Windows
☀️ 学会编程入门必备 C# 最基础知识介绍—— C# 高级文件操作(文本文件的读写、二进制文件的读写、Windows 文件系统的操作)
前言🙏 C# 文本文件的读写👇 StreamWriter 类 C# 二进制文件的读写👏 BinaryReader 类 BinaryWriter 类 C# Windows 文件系统的操作👋 DirectoryInfo 类 FileInfo 类
☀️ 学会编程入门必备 C# 最基础知识介绍—— C# 高级文件操作(文本文件的读写、二进制文件的读写、Windows 文件系统的操作)
|
开发框架 安全 .NET
C#多线程(10):读写锁
C#多线程(10):读写锁
407 0
|
C#
C#读写Excel
读 Range ce=sheet.Cells[2,3]; string vv=ce.value; 写 //注意: // * Excel中形如Cells[x][y]的写法,前面的数字是RowIndex,后面的数字是ColumnInde...
1208 0
|
分布式数据库 API
C# API方式串口读写
我的串口程序修改为API方式后,实际发现与SerialPort类遇到同样的问题,所以SerialPort类还是值得信任的。该API方式的代码在WinCE平台和PC平台都调试通过
1087 0