C#進階——記一次USB HID的各種坑(x86,x64,win10,win7)

vv彭發表於2022-01-19

一、簡敘

寫工控上位機的搬磚人,難免會遇到USB通訊,在一個專案中,我寫的上位機使用USB HID協議和STM32通訊傳輸資料,從零大概花了幾天找例程,找資料,最後是各種搬磚修補,終於出來了一個出版DOME,能和下位機實時通訊了。

HID通訊方式其實很常見,像滑鼠、鍵盤等待外設都是這種方式,我也不知道為啥要用這種方式,我當初用它是因為其傳輸速率比串列埠快(憶當初下位機還沒開發出網口的苦日子),再則其是免驅(win內建驅動,直接調API即可)。

HID識別裝置主要通過裝置的PID(廠商ID)和VID(產品ID),進而區分。

 

二、主體程式

網上找了很多版本,主體程式都是大同小異,最後我便借鑑了一篇博文,來砌我的主體程式。

很不出意外,砌出來的東西不行。記得以前聽前輩說過一句話,你的薪資多高取決於你填坑的能力有多強。

現在想想確實如此,在這個網際網路的時代,搬磚並不是一件難事,但是不能確保搬來的磚不是每一塊都適合你,你必須要修修補補再用,所以修補的能力便成了你賺錢的能力。

還有我奉獻各位年輕氣旺年輕人,不要急於求成,網上找到原始碼不是難事,必須一步一個腳印,把原始碼啃了,不要以為會搬磚就老子天下第一,那有多難的事呀。(沒錯,我以前包括現在都是這麼一個爛人,正努力扭正態度)。

我們不重複造車輪,但我們必須有造車輪的能力,出來混,遲早要跪的。

下面我貼上我的主要原始碼部分,基本和其他博文的一樣:

namespace HID
{
    #region 結構體
    /// <summary>
    /// SP_DEVICE_INTERFACE_DATA structure defines a device interface in a device information set.
    /// </summary>
    public struct SP_DEVICE_INTERFACE_DATA
    {
        public int cbSize;
        public Guid interfaceClassGuid;
        public int flags;
        public int reserved;
    }

    /// <summary>
    /// SP_DEVICE_INTERFACE_DETAIL_DATA structure contains the path for a device interface.
    /// </summary>
    [StructLayout(LayoutKind.Sequential, Pack = 2)]
    internal struct SP_DEVICE_INTERFACE_DETAIL_DATA
    {
        internal int cbSize;
        internal short devicePath;
    }

    /// <summary>
    /// SP_DEVINFO_DATA structure defines a device instance that is a member of a device information set.
    /// </summary>
    [StructLayout(LayoutKind.Sequential)]
    public class SP_DEVINFO_DATA
    {
        public int cbSize = Marshal.SizeOf(typeof(SP_DEVINFO_DATA));
        public Guid classGuid = Guid.Empty; // temp
        public int devInst = 0; // dumy
        public int reserved = 0;
    }
    /// <summary>
    /// Flags controlling what is included in the device information set built by SetupDiGetClassDevs
    /// </summary>
    public enum DIGCF
    {
        DIGCF_DEFAULT = 0x00000001, // only valid with DIGCF_DEVICEINTERFACE                 
        DIGCF_PRESENT = 0x00000002,
        DIGCF_ALLCLASSES = 0x00000004,
        DIGCF_PROFILE = 0x00000008,
        DIGCF_DEVICEINTERFACE = 0x00000010
    }
    /// <summary>
    /// The HIDD_ATTRIBUTES structure contains vendor information about a HIDClass device
    /// </summary>
    public struct HIDD_ATTRIBUTES
    {
        public int Size;
        public ushort VendorID;
        public ushort ProductID;
        public ushort VersionNumber;
    }

    public struct HIDP_CAPS
    {
        public ushort Usage;
        public ushort UsagePage;
        public ushort InputReportByteLength;
        public ushort OutputReportByteLength;
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 17)]
        public ushort[] Reserved;
        public ushort NumberLinkCollectionNodes;
        public ushort NumberInputButtonCaps;
        public ushort NumberInputValueCaps;
        public ushort NumberInputDataIndices;
        public ushort NumberOutputButtonCaps;
        public ushort NumberOutputValueCaps;
        public ushort NumberOutputDataIndices;
        public ushort NumberFeatureButtonCaps;
        public ushort NumberFeatureValueCaps;
        public ushort NumberFeatureDataIndices;
    }
    /// <summary>
    /// Type of access to the object. 
    ///</summary>
    static class DESIREDACCESS
    {
        public const uint GENERIC_READ = 0x80000000;
        public const uint GENERIC_WRITE = 0x40000000;
        public const uint GENERIC_EXECUTE = 0x20000000;
        public const uint GENERIC_ALL = 0x10000000;
    }
    /// <summary>
    /// Action to take on files that exist, and which action to take when files do not exist. 
    /// </summary>
    static class CREATIONDISPOSITION
    {
        public const uint CREATE_NEW = 1;
        public const uint CREATE_ALWAYS = 2;
        public const uint OPEN_EXISTING = 3;
        public const uint OPEN_ALWAYS = 4;
        public const uint TRUNCATE_EXISTING = 5;
    }
    /// <summary>
    /// File attributes and flags for the file. 
    /// </summary>
    static class FLAGSANDATTRIBUTES
    {
        public const uint FILE_FLAG_WRITE_THROUGH = 0x80000000;
        public const uint FILE_FLAG_OVERLAPPED = 0x40000000;
        public const uint FILE_FLAG_NO_BUFFERING = 0x20000000;
        public const uint FILE_FLAG_RANDOM_ACCESS = 0x10000000;
        public const uint FILE_FLAG_SEQUENTIAL_SCAN = 0x08000000;
        public const uint FILE_FLAG_DELETE_ON_CLOSE = 0x04000000;
        public const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
        public const uint FILE_FLAG_POSIX_SEMANTICS = 0x01000000;
        public const uint FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000;
        public const uint FILE_FLAG_OPEN_NO_RECALL = 0x00100000;
        public const uint FILE_FLAG_FIRST_PIPE_INSTANCE = 0x00080000;
    }
    /// <summary>
    /// Serves as a standard header for information related to a device event reported through the WM_DEVICECHANGE message.
    /// </summary>
    [StructLayout(LayoutKind.Sequential)]
    public struct DEV_BROADCAST_HDR
    {
        public int dbcc_size;
        public int dbcc_devicetype;
        public int dbcc_reserved;
    }
    /// <summary>
    /// Contains information about a class of devices
    /// </summary>
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    public struct DEV_BROADCAST_DEVICEINTERFACE
    {
        public int dbcc_size;
        public int dbcc_devicetype;
        public int dbcc_reserved;
        public Guid dbcc_classguid;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 255)]
        public string dbcc_name;
    }

    static class DEVICE_FLAG
    {
        /// <summary>Windows message sent when a device is inserted or removed</summary>
        public const int WM_DEVICECHANGE = 0x0219;
        /// <summary>WParam for above : A device was inserted</summary>
        public const int DEVICE_ARRIVAL = 0x8000;
        /// <summary>WParam for above : A device was removed</summary>
        public const int DEVICE_REMOVECOMPLETE = 0x8004;
        /// <summary>Used when registering for device insert/remove messages : specifies the type of device</summary>
        public const int DEVTYP_DEVICEINTERFACE = 0x05;
        /// <summary>Used when registering for device insert/remove messages : we're giving the API call a window handle</summary>
        public const int DEVICE_NOTIFY_WINDOW_HANDLE = 0;
    }


    /// <summary>
    /// Sharing mode of the file or object
    ///</summary>
    static class SHAREMODE
    {
        public const uint FILE_SHARE_READ = 0x00000001;
        public const uint FILE_SHARE_WRITE = 0x00000002;
        public const uint FILE_SHARE_DELETE = 0x00000004;
    }
    #endregion

    /// <summary>
    /// 傳入ID和arraybuff(只讀)
    /// </summary>
    public class report : EventArgs
    {

        public readonly byte reportID; //接收傳送的資料(第一幀)
        public readonly byte[] reportBuff; //接收傳送的資料(第二幀及以後)
        public report(byte id, byte[] arrayBuff)
        {
            reportID = id;
            reportBuff = arrayBuff;
        }
    }

    public class Hid
    {
        #region 宣告winAPI
        // **************************以下是呼叫windows的API的函式**********************************
        /// <summary>
        /// The HidD_GetHidGuid routine returns the device interface GUID for HIDClass devices.
        /// </summary>
        /// <param name="HidGuid">a caller-allocated GUID buffer that the routine uses to return the device interface GUID for HIDClass devices.</param>
        [DllImport("hid.dll")]
        private static extern void HidD_GetHidGuid(ref Guid HidGuid);

        /// <summary>
        /// The SetupDiGetClassDevs function returns a handle to a device information set that contains requested device information elements for a local machine. 
        /// </summary>
        /// <param name="ClassGuid">GUID for a device setup class or a device interface class. </param>
        /// <param name="Enumerator">A pointer to a NULL-terminated string that supplies the name of a PnP enumerator or a PnP device instance identifier. </param>
        /// <param name="HwndParent">A handle of the top-level window to be used for a user interface</param>
        /// <param name="Flags">A variable  that specifies control options that filter the device information elements that are added to the device information set. </param>
        /// <returns>a handle to a device information set </returns>
        [DllImport("setupapi.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr SetupDiGetClassDevs(ref Guid ClassGuid, uint Enumerator, IntPtr HwndParent, DIGCF Flags);

        /// <summary>
        /// The SetupDiDestroyDeviceInfoList function deletes a device information set and frees all associated memory.
        /// </summary>
        /// <param name="DeviceInfoSet">A handle to the device information set to delete.</param>
        /// <returns>returns TRUE if it is successful. Otherwise, it returns FALSE </returns>
        [DllImport("setupapi.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern Boolean SetupDiDestroyDeviceInfoList(IntPtr deviceInfoSet);

        /// <summary>
        /// The SetupDiEnumDeviceInterfaces function enumerates the device interfaces that are contained in a device information set. 
        /// </summary>
        /// <param name="deviceInfoSet">A pointer to a device information set that contains the device interfaces for which to return information</param>
        /// <param name="deviceInfoData">A pointer to an SP_DEVINFO_DATA structure that specifies a device information element in DeviceInfoSet</param>
        /// <param name="interfaceClassGuid">a GUID that specifies the device interface class for the requested interface</param>
        /// <param name="memberIndex">A zero-based index into the list of interfaces in the device information set</param>
        /// <param name="deviceInterfaceData">a caller-allocated buffer that contains a completed SP_DEVICE_INTERFACE_DATA structure that identifies an interface that meets the search parameters</param>
        /// <returns></returns>
        [DllImport("setupapi.dll", CharSet = CharSet.Auto, SetLastError = false)]
        private static extern Boolean SetupDiEnumDeviceInterfaces(IntPtr deviceInfoSet, IntPtr deviceInfoData, ref Guid interfaceClassGuid, uint memberIndex, ref SP_DEVICE_INTERFACE_DATA deviceInterfaceData);

        /// <summary>
        /// The SetupDiGetDeviceInterfaceDetail function returns details about a device interface.
        /// </summary>
        /// <param name="deviceInfoSet">A pointer to the device information set that contains the interface for which to retrieve details</param>
        /// <param name="deviceInterfaceData">A pointer to an SP_DEVICE_INTERFACE_DATA structure that specifies the interface in DeviceInfoSet for which to retrieve details</param>
        /// <param name="deviceInterfaceDetailData">A pointer to an SP_DEVICE_INTERFACE_DETAIL_DATA structure to receive information about the specified interface</param>
        /// <param name="deviceInterfaceDetailDataSize">The size of the DeviceInterfaceDetailData buffer</param>
        /// <param name="requiredSize">A pointer to a variable that receives the required size of the DeviceInterfaceDetailData buffer</param>
        /// <param name="deviceInfoData">A pointer buffer to receive information about the device that supports the requested interface</param>
        /// <returns></returns>
        [DllImport("setupapi.dll", SetLastError = false, 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>
        /// The HidD_GetAttributes routine returns the attributes of a specified top-level collection.
        /// </summary>
        /// <param name="HidDeviceObject">Specifies an open handle to a top-level collection</param>
        /// <param name="Attributes">a caller-allocated HIDD_ATTRIBUTES structure that returns the attributes of the collection specified by HidDeviceObject</param>
        /// <returns></returns>
        [DllImport("hid.dll")]
        private static extern Boolean HidD_GetAttributes(IntPtr hidDeviceObject, out HIDD_ATTRIBUTES attributes);
        /// <summary>
        /// The HidD_GetSerialNumberString routine returns the embedded string of a top-level collection that identifies the serial number of the collection's physical device.
        /// </summary>
        /// <param name="HidDeviceObject">Specifies an open handle to a top-level collection</param>
        /// <param name="Buffer">a caller-allocated buffer that the routine uses to return the requested serial number string</param>
        /// <param name="BufferLength">Specifies the length, in bytes, of a caller-allocated buffer provided at Buffer</param>
        /// <returns></returns>
        [DllImport("hid.dll")]
        private static extern Boolean HidD_GetSerialNumberString(IntPtr hidDeviceObject, IntPtr buffer, int bufferLength);

        /// <summary>
        /// The HidD_GetPreparsedData routine returns a top-level collection's preparsed data.
        /// </summary>
        /// <param name="hidDeviceObject">Specifies an open handle to a top-level collection. </param>
        /// <param name="PreparsedData">Pointer to the address of a routine-allocated buffer that contains a collection's preparsed data in a _HIDP_PREPARSED_DATA structure.</param>
        /// <returns>HidD_GetPreparsedData returns TRUE if it succeeds; otherwise, it returns FALSE.</returns>
        [DllImport("hid.dll")]
        private static extern Boolean HidD_GetPreparsedData(IntPtr hidDeviceObject, out IntPtr PreparsedData);

        [DllImport("hid.dll")]
        private static extern Boolean HidD_FreePreparsedData(IntPtr PreparsedData);

        [DllImport("hid.dll")]
        private static extern uint HidP_GetCaps(IntPtr PreparsedData, out HIDP_CAPS Capabilities);


        /// <summary>
        /// This function creates, opens, or truncates a file, COM port, device, service, or console. 
        /// </summary>
        /// <param name="fileName">a null-terminated string that specifies the name of the object</param>
        /// <param name="desiredAccess">Type of access to the object</param>
        /// <param name="shareMode">Share mode for object</param>
        /// <param name="securityAttributes">Ignored; set to NULL</param>
        /// <param name="creationDisposition">Action to take on files that exist, and which action to take when files do not exist</param>
        /// <param name="flagsAndAttributes">File attributes and flags for the file</param>
        /// <param name="templateFile">Ignored</param>
        /// <returns>An open handle to the specified file indicates success</returns>
        [DllImport("kernel32.dll", SetLastError = false)]
        private static extern IntPtr CreateFile(string fileName, uint desiredAccess, uint shareMode, uint securityAttributes, uint creationDisposition, uint flagsAndAttributes, uint templateFile);

        /// <summary>
        /// This function closes an open object handle.
        /// </summary>
        /// <param name="hObject">Handle to an open object</param>
        /// <returns></returns>
        [DllImport("kernel32.dll")]
        private static extern int CloseHandle(IntPtr hObject);

        /// <summary>
        /// This function reads data from a file, starting at the position indicated by the file pointer.
        /// </summary>
        /// <param name="file">Handle to the file to be read</param>
        /// <param name="buffer">Pointer to the buffer that receives the data read from the file </param>
        /// <param name="numberOfBytesToRead">Number of bytes to be read from the file</param>
        /// <param name="numberOfBytesRead">Pointer to the number of bytes read</param>
        /// <param name="lpOverlapped">Unsupported; set to NULL</param>
        /// <returns></returns>
        [DllImport("Kernel32.dll", SetLastError = false)]
        private static extern bool ReadFile(IntPtr file, byte[] buffer, uint numberOfBytesToRead, out uint numberOfBytesRead, IntPtr lpOverlapped);

        /// <summary>
        ///  This function writes data to a file
        /// </summary>
        /// <param name="file">Handle to the file to be written to</param>
        /// <param name="buffer">Pointer to the buffer containing the data to write to the file</param>
        /// <param name="numberOfBytesToWrite">Number of bytes to write to the file</param>
        /// <param name="numberOfBytesWritten">Pointer to the number of bytes written by this function call</param>
        /// <param name="lpOverlapped">Unsupported; set to NULL</param>
        /// <returns></returns>
        [DllImport("Kernel32.dll", SetLastError = false)]
        private static extern bool WriteFile(IntPtr file, byte[] buffer, uint numberOfBytesToWrite, out uint numberOfBytesWritten, IntPtr lpOverlapped);

        /// <summary>
        /// Registers the device or type of device for which a window will receive notifications
        /// </summary>
        /// <param name="recipient">A handle to the window or service that will receive device events for the devices specified in the NotificationFilter parameter</param>
        /// <param name="notificationFilter">A pointer to a block of data that specifies the type of device for which notifications should be sent</param>
        /// <param name="flags">A Flags that specify the handle type</param>
        /// <returns>If the function succeeds, the return value is a device notification handle</returns>
        [DllImport("User32.dll", SetLastError = false)]
        private static extern IntPtr RegisterDeviceNotification(IntPtr recipient, IntPtr notificationFilter, int flags);

        /// <summary>
        /// Closes the specified device notification handle.
        /// </summary>
        /// <param name="handle">Device notification handle returned by the RegisterDeviceNotification function</param>
        /// <returns></returns>
        [DllImport("user32.dll", SetLastError = false)]
        private static extern bool UnregisterDeviceNotification(IntPtr handle);
        #endregion


        #region 定義欄位/屬性
        public UInt16 VID = 0x0483;
        public UInt16 PID = 0x5750;
        private IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1);
        private const int MAX_USB_DEVICES = 64;
        private bool deviceOpened = false;
        private FileStream hidDevice = null; //建立一個usb物件
        //public IntPtr[] devices = new IntPtr[10]; //儲存已開啟可讀寫的裝置
        List<string> deviceList = new List<string>(); //儲存所有裝置
        IntPtr device; //開啟檔案的裝置
        private bool IsRead = true; //允許讀取標誌
        int outputReportLength;//輸出報告長度,包刮一個位元組的報告ID
        public int OutputReportLength { get { return outputReportLength; } } //資料長度
        int inputReportLength;//輸入報告長度,包刮一個位元組的報告ID   
        public int InputReportLength { get { return inputReportLength; } }
        private Guid device_class;
        #endregion

        /// <summary>
        /// 建構函式
        /// </summary>
        public Hid()
        {
            device_class = HIDGuid;
        }

        #region 封裝函式
        /// <summary>
        /// 遍歷所有裝置,返回所有可用裝置的VID/PID到字串,以“\r\n”分割
        /// </summary>
        /// <param name="deviceCount">裝置數量</param>
        /// <returns></returns>
        public String ListHidDevice(ref int deviceCount)
        {
            //獲取連線的HID列表
            string str = "";
            deviceCount = 0;
            deviceList.Clear();
            GetHidDeviceList(ref deviceList);//呼叫函式獲取所有連線裝置並返回給實參列表deviceList
            //遍歷所有裝置
            for (int i = 0; i < deviceList.Count; i++)
            {
                //開啟裝置檔案(可讀寫)並返回控制程式碼到device變數
                device = CreateFile(deviceList[i], DESIREDACCESS.GENERIC_READ | DESIREDACCESS.GENERIC_WRITE, 0, 0, CREATIONDISPOSITION.OPEN_EXISTING, FLAGSANDATTRIBUTES.FILE_FLAG_OVERLAPPED, 0);
                if (device != INVALID_HANDLE_VALUE)
                {
                    //devices[j] = device;
                    //j++;
                    HIDD_ATTRIBUTES attributes;
                    HidD_GetAttributes(device, out attributes);
                    str += "VID:" + attributes.VendorID.ToString("X4") + " PID:" + attributes.ProductID.ToString("X4") + "\r\n";
                    deviceCount++;
                    CloseHandle(device); //釋放開啟的控制程式碼
                }
            }
            if (deviceCount == 0)
            {
                return "Unable to find equipment!";
            }
            return str;
        }

        /// <summary>
        /// 開啟指定資訊的裝置
        /// </summary>
        /// <param name="vID">裝置的vID</param>
        /// <param name="pID">裝置的pID</param>
        public HID_RETURN OpenDevice(UInt16 vID, UInt16 pID)
        {
            if (deviceOpened == false)
            {
                for (int i = 0; i < deviceList.Count; i++)
                {
                    //開啟裝置檔案(可讀寫)並返回控制程式碼到device變數
                    device = CreateFile(deviceList[i], DESIREDACCESS.GENERIC_READ | DESIREDACCESS.GENERIC_WRITE, 0, 0, CREATIONDISPOSITION.OPEN_EXISTING, FLAGSANDATTRIBUTES.FILE_FLAG_OVERLAPPED, 0);
                    HIDD_ATTRIBUTES attributes;
                    HidD_GetAttributes(device, out attributes);
                    if (attributes.VendorID == vID && attributes.ProductID == pID)   // && deviceStr == serial
                    {
                        IntPtr preparseData; //測試長度
                        HIDP_CAPS caps; //結構體
                        HidD_GetPreparsedData(device, out preparseData); //獲取
                        HidP_GetCaps(preparseData, out caps); //獲取的結果傳給結構體
                        HidD_FreePreparsedData(preparseData); //釋放
                        outputReportLength = caps.OutputReportByteLength; //賦值輸出長度
                        inputReportLength = caps.InputReportByteLength; //賦值輸入長度
                        hidDevice = new FileStream(new SafeFileHandle(device, false), FileAccess.ReadWrite, inputReportLength, true); //例項化sub物件,物件為欄位,所以全域性呼叫同一個,這裡已經定了讀資料長度
                        deviceOpened = true;
                        IsRead = true; //允許讀取
                        BeginAsyncRead(); //開啟成功馬上自動讀取
                        //返回開啟成功
                        return HID_RETURN.SUCCESS;
                    }
                    else
                    {
                        CloseHandle(device); //釋放開啟的控制程式碼
                    }
                }
                //返回開啟失敗
                return HID_RETURN.DEVICE_NOT_FIND;
            }
            //返回已開啟
            else
                return HID_RETURN.DEVICE_OPENED;
        }

        /// <summary>
        /// 停止讀取
        /// </summary>
        public void StopRead()
        {
            IsRead = false; //禁止讀取
        }

        /// <summary>
        /// 開始讀取
        /// </summary>
        public void StatiRead()
        {
            IsRead = true; //允許讀取
        }
        /// <summary>
        /// 關閉開啟的裝置
        /// </summary>
        public void CloseDevice()
        {
            IsRead = false; //禁止讀取
            System.Threading.Thread.Sleep(100); //執行緒等待,等待資料讀完在關閉裝置
            if (deviceOpened == true)
            {
                hidDevice.Close(); //關閉usb
                deviceOpened = false;
                CloseHandle(device); //釋放開啟的控制程式碼
            }
        }

        /// <summary>
        /// 開始一次非同步讀
        /// </summary>
        private void BeginAsyncRead()
        {
            byte[] inputBuff = new byte[InputReportLength];
            if (IsRead)
            {
                try
                {
                    //非同步讀資料,快取區、開始讀的幀、最大讀取長度、讀完非同步呼叫的方法、將該特定的非同步讀取請求與其他請求區別開來
                    hidDevice.BeginRead(inputBuff, 0, InputReportLength, new AsyncCallback(ReadCompleted), inputBuff);
                }
                catch (Exception)
                {
                    throw;
                }
            }
        }

        List<byte[]> buffer = new List<byte[]>();
        /// <summary>
        /// 非同步讀取結束,發出有資料到達事件
        /// </summary>
        /// <param name="iResult">這裡是輸入報告的陣列</param>
        private void ReadCompleted(IAsyncResult iResult)
        {
            byte[] readBuff = (byte[])(iResult.AsyncState); //裝載資料
            buffer.Add(readBuff);
            if (IsRead)
            {
                try
                {
                    try
                    {
                        hidDevice.EndRead(iResult);//讀取結束,如果讀取錯誤就會產生一個異常
                        byte[] reportData = new byte[readBuff.Length - 1];
                        //再次從第二幀開始裝載
                        for (int i = 1; i < readBuff.Length; i++)
                            reportData[i - 1] = readBuff[i];
                        report e = new report(readBuff[0], reportData);
                        OnDataReceived(e); //觸發事件
                    }
                    finally
                    {
                        BeginAsyncRead();//啟動下一次讀操作
                    }
                }
                catch (IOException)//讀寫錯誤,裝置已經被移除
                {
                    EventArgs ex = new EventArgs();
                    OnDeviceRemoved(ex);//發出裝置移除訊息
                    //CloseDevice();
                }
            }
        }

        /// <summary>
        /// 事件:資料到達,處理此事件以接收輸入資料
        /// </summary>
        public event EventHandler<report> DataReceived;

        /// <summary>
        /// 呼叫資料到達事件方法
        /// </summary>
        /// <param name="e"></param>
        protected virtual void OnDataReceived(report e)
        {
            DataReceived?.Invoke(this, e);
        }

        /// <summary>
        /// 事件:設定連線
        /// </summary>
        public event EventHandler DeviceArrived;
        protected virtual void OnDeviceArrived(EventArgs e)
        {
            DeviceArrived?.Invoke(this, e);
        }

        /// <summary>
        /// 事件:裝置斷開
        /// </summary>
        public event EventHandler DeviceRemoved;
        protected virtual void OnDeviceRemoved(EventArgs e)
        {
            DeviceRemoved?.Invoke(this, e);
        }

        /// <summary>
        /// 傳送資料函式
        /// </summary>
        /// <param name="buffer"></param>
        /// <returns></returns>
        public HID_RETURN Write(report r)
        {
            if (deviceOpened)
            {
                try
                {
                    byte[] buffer = new byte[outputReportLength]; //儲存資料位
                    buffer[0] = r.reportID; //資料位第一幀
                    int maxBufferLength = 0; //迴圈賦值
                    //判斷資料長度-1 與 賦值列表長度-1的大小,迴圈賦值以最小的長度為準
                    if (r.reportBuff.Length < outputReportLength - 1)
                        maxBufferLength = r.reportBuff.Length;
                    else
                        maxBufferLength = outputReportLength;
                    //遍歷賦值資料位第二幀及以後的資料
                    for (int i = 1; i < maxBufferLength; i++)
                        buffer[i] = r.reportBuff[i - 1];
                    hidDevice.Write(buffer, 0, OutputReportLength); //快取區、開始讀的幀數、最大資料長度
                    IsRead = true;//允許讀取
                    BeginAsyncRead(); //繼續讀取
                }
                catch
                {
                    EventArgs ex = new EventArgs();
                    OnDeviceRemoved(ex);//發出裝置移除訊息
                    CloseDevice();
                }
            }
            return HID_RETURN.WRITE_FAILD;
        }

        /// <summary>
        /// 獲取所有連線的hid的裝置路徑
        /// </summary>
        /// <returns>包含每個裝置路徑的字串陣列</returns>
        public static void GetHidDeviceList(ref List<string> deviceList)
        {
            Guid hUSB = Guid.Empty;
            uint index = 0;
            deviceList.Clear();
            // 取得hid裝置全域性id
            HidD_GetHidGuid(ref hUSB);
            //取得一個包含所有HID介面資訊集合的控制程式碼
            IntPtr hidInfoSet = SetupDiGetClassDevs(ref hUSB, 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);
                //查詢集合中每一個介面
                for (index = 0; index < MAX_USB_DEVICES; index++)
                {
                    //得到第index個介面資訊
                    if (SetupDiEnumDeviceInterfaces(hidInfoSet, IntPtr.Zero, ref hUSB, index, ref interfaceInfo))
                    {
                        int buffsize = 0;
                        // 取得介面詳細資訊:第一次讀取錯誤,但可以取得資訊緩衝區的大小
                        SetupDiGetDeviceInterfaceDetail(hidInfoSet, ref interfaceInfo, IntPtr.Zero, buffsize, ref buffsize, null);
                        //構建接收緩衝
                        IntPtr pDetail = Marshal.AllocHGlobal(buffsize);
                        SP_DEVICE_INTERFACE_DETAIL_DATA detail = new SP_DEVICE_INTERFACE_DETAIL_DATA();
                        detail.cbSize = Marshal.SizeOf(typeof(SP_DEVICE_INTERFACE_DETAIL_DATA));
                        Marshal.StructureToPtr(detail, pDetail, false);
                        if (SetupDiGetDeviceInterfaceDetail(hidInfoSet, ref interfaceInfo, pDetail, buffsize, ref buffsize, null))
                        {
                            deviceList.Add(Marshal.PtrToStringAuto((IntPtr)((int)pDetail + 4)));
                        }
                        Marshal.FreeHGlobal(pDetail);
                    }
                }
            }
            SetupDiDestroyDeviceInfoList(hidInfoSet);
            //return deviceList.ToArray();
        }

        ///// <summary>
        ///// 檢測裝置連線狀態
        ///// </summary>
        ///// <param name="m"></param>
        //public void ParseMessages(ref Message m)
        //{
        //    if (m.Msg == DEVICE_FLAG.WM_DEVICECHANGE)    // we got a device change message! A USB device was inserted or removed
        //    {
        //        switch (m.WParam.ToInt32())    // Check the W parameter to see if a device was inserted or removed
        //        {
        //            case DEVICE_FLAG.DEVICE_ARRIVAL:    // inserted
        //                Console.WriteLine("ParseMessages 裝置連線");
        //                if (DeviceArrived != null)
        //                {
        //                    DeviceArrived(this, new EventArgs());
        //                }
        //                CheckDevicePresent(VID, PID);
        //                break;
        //            case DEVICE_FLAG.DEVICE_REMOVECOMPLETE:    // removed
        //                Console.WriteLine("ParseMessages 裝置拔除");
        //                if (DeviceRemoved != null)
        //                {
        //                    DeviceRemoved(this, new EventArgs());
        //                }
        //                CheckDevicePresent(VID, PID);
        //                break;
        //        }
        //    }
        //}

        /// <summary>
        /// 開啟指定裝置
        /// </summary>
        /// <param name="parameterVID">裝置的VID</param>
        /// <param name="parameterPID">裝置的PID</param>
        /// <returns>開啟成功與否</returns>
        public bool CheckDevicePresent(ushort parameterVID, ushort parameterPID)
        {
            this.VID = parameterVID;
            this.PID = parameterPID;
            HID_RETURN hid_ret;
            try
            {
                hid_ret = OpenDevice(VID, PID);
                if (hid_ret == HID_RETURN.SUCCESS)
                {
                    return true;
                }
                else
                {
                    return false;
                }
            }
            catch (Exception)
            {
                throw;
            }
        }

        /// <summary>
        /// Helper to get the HID guid.
        /// </summary>
        public static Guid HIDGuid
        {
            get
            {
                Guid gHid = Guid.Empty;
                HidD_GetHidGuid(ref gHid);
                return gHid;
                //return new Guid("A5DCBF10-6530-11D2-901F-00C04FB951ED"); //gHid;
            }
        }

        /// <summary>
        /// btye陣列轉換為字串
        /// </summary>
        /// <param name="bytes"></param>
        /// <returns></returns>
        public static string ByteToHexString(byte[] bytes)
        {
            string str = string.Empty;
            if (bytes != null)
            {
                for (int i = 0; i < bytes.Length; i++)
                {
                    str += bytes[i].ToString("X2");
                }
            }
            return str;
        }

        public enum HID_RETURN
        {
            SUCCESS = 0,
            NO_DEVICE_CONECTED,
            DEVICE_NOT_FIND,
            DEVICE_OPENED,
            WRITE_FAILD,
            READ_FAILD
        }
        #endregion

    }
}

 

 

三、坑之總結

我簡單地總結了一下,坑有以下幾個:

1.接收資料長度沒對齊,導致接收不成功;

2.Win10除錯成功,Win7除錯失敗;

3.32程式執行成功,64位程式執行失敗;

4.非同步接收資料順序錯亂。

 

四、談坑

1.坑之接收不成

實話說,我也忘了為什麼接收資料不成功,修改了哪裡。因為我是在一年前寫的這個程式,而最近同時需要我封的程式碼,且他的程式是64位的,我之前是32位,故不相容,好人做到底,我就幫他稍微改了一下,相容進去了。然後寫下這篇隨筆記錄一下情況,方便追溯。

下面是我憑藉零星的回憶,一頓亂說,我實在忘了原版(接收不成功)長啥樣了。

接收資料不成功主要有兩個原因,一個是接收資料的長度不對,另一個也是接收長度不對。我這裡不是學魯迅大師說話,兩種情況是不一樣的。

第一個接收長度不對是因為傳送長度超過了64byte,HID幀資料不能超過64Byte,我也不知到為什麼,親測確實如此,如果有興趣可以深究一下。

第二個報文的長度不對,

 //非同步讀資料,快取區、開始讀的幀、最大讀取長度、讀完非同步呼叫的方法、將該特定的非同步讀取請求與其他請求區別開來
 hidDevice.BeginRead(inputBuff, 0, InputReportLength, new AsyncCallback(ReadCompleted), inputBuff); 

上面的“InputReportLength”必須和下面的“inputReportLength”相同,其是報文的長度。

outputReportLength = caps.OutputReportByteLength; //賦值輸出長度
inputReportLength = caps.InputReportByteLength; //賦值輸入長度

 

2.坑之win7傳送失敗

這個坑確實很坑爹,之前在win10下用得好好的,後面來了一臺win7的工控機,然後就傳送不了資料了。

後面把第一幀資料改成“0x00”,又可以了,就那麼神奇,哎!

 

3.坑之x64框架列舉裝置失敗

這個坑,其實也不叫坑,只能說明我的技術不行,被虐得完無體膚!網上查了海量資料,遇到我這個問題的不多,並且大部分回答都是把x64改成x86,顯然這並不是我想要的答案。還有少部分人說是winAPI呼叫錯了,改成x64要改成x64的API,然後又給出了一些修改特性的方式,只能說,統統不行!我理解是,同樣的宣告方式,win系統會更加我們軟體框架不一樣,自動呼叫相應winAPI,x86的API在C:\Windows\SysWOW64資料夾裡,而x64的API在C:\Windows\System32資料夾裡。

x86和x64的資料長度是有差異的,包括指標的長度都是不一樣。

我在x86下,呼叫“SetupDiGetClassDevs”獲取裝置資訊集的控制程式碼時返回的地址大概是9位數左右,而在x64中,返回的地址大概是13位數。

IntPtr hidInfoSet = SetupDiGetClassDevs(ref hUSB, 0, IntPtr.Zero, DIGCF.DIGCF_PRESENT | DIGCF.DIGCF_DEVICEINTERFACE);

當列舉列表中的裝置時,返回false!

SetupDiEnumDeviceInterfaces(hidInfoSet, IntPtr.Zero, ref hUSB, index, ref interfaceInfo)

我們會想到一個宣告控制程式碼的型別“IntPtr”,平臺特定的整數型別。資源的大小取決於使用的硬體和作業系統,即此型別的例項在32位硬體和作業系統中將是32位,在64位硬體和作業系統中將是64位;但其大小總是足以包含系統的指標(因此也可以包含資源的名稱),關心的小夥伴可以深入瞭解一下。

問題又縮小了,肯定是哪個型別宣告有問題。細分析了一下,

SetupDiEnumDeviceInterfaces中五個引數中,前面四個都沒有問題,問題肯定出在最後一個引數“interfaceInfo”,而最後一個引數是SP_DEVICE_INTERFACE_DATA型別的結構體,那問題肯定在結構體中某個成員,再查了一下這個結構體,最後一個成員是指標型別“reserved”
  public struct SP_DEVICE_INTERFACE_DATA
    {
        public int cbSize;
        public Guid interfaceClassGuid;
        public int flags;
        public int reserved;
    }

於是把“reserved”的型別改成IntPtr型別即可,果然能通過列舉。後面又用同樣的方式修改了其他結構體“reserved”的型別。

這裡還有個小問題,上面結構體的其他成員型別不要修改,我之前把int改成uint,報記憶體溢位的錯誤。

 

4.坑之接收資料順序錯亂

如前面所言,我用usb主要是考慮傳輸速度,所以下位機發上來的頻率基本是1ms一次,貌似非同步接收還是反應不錯的。

但是解析資料就有點和想的不太一樣了,我把資料採集上來,用動態波形圖顯示在上位機,但是顯示時明顯發現資料是不對的,後面監聽發現資料排序的有問題,比如後一幀資料比前一幀資料解析完成早。

後面利用開了三個執行緒來解決這個問題,一個執行緒專門接收資料(非同步接收),接到立馬放到list裡,然後開一個執行緒解析資料,再開一個執行緒顯示。這裡必須要做多執行緒同步,不然還是沒有達到我們的要求。我是用執行緒的訊號來處理他們之間同步的,因為這裡涉及的東西比較多,後面有空再單獨整理。

 

五、學不完的新知識,調不完的BUG

雖然解決了幾個HID的坑,目前來說是沒有多大問題,但是並不能保證以後沒有新的需求新的bug出現,革命尚未完成,同志仍需努力!

本人並非計算機科班出身,對於程式設計也不是說很熱愛,曾經年少時,覺得黑客很牛逼,很酷,於是就踏上了這一條“不歸路”,學不完的新知識,調不完的新bug,應了那句,從入門到入土。

我們必須要放低態度,以無知,無我的狀態修身,才能知之,自我!

 

相關文章