我是靠谱客的博主 称心大树,最近开发中收集的这篇文章主要介绍USB 设备热插拔的检测,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

系统检测USB 设备往往分为两个过程:

 

1. USB 设备已经插入, 打开软件。 此时需要枚举当前设备列表中所有的设备,并过滤指定的USB 设备。

2. 打开软件后, USB 热插拔 。 此时用WM_DEVICECHANGE 消息去获取设备信息。 当设备插入或者移除时,系统给当前主窗口发送WM_DEVICECHANGE 消息 。 注意: 默认状态, 任何设备都会通知。 所以需要注册指定的通知消息。

 

第一种:

枚举系统中所有的USB 设备, 可根据 PID,VID 进一步做判断。

void CDataSyncAccessDlg::CollectUSBInfo()
{
	// 获取当前系统所有使用的设备
	DWORD dwFlag = (DIGCF_ALLCLASSES | DIGCF_PRESENT);
	HDEVINFO hDevInfo = SetupDiGetClassDevs(NULL, NULL, NULL, dwFlag);
	if (INVALID_HANDLE_VALUE == hDevInfo)
	{
		AfxMessageBox(_T("获取系统设备列表失败"));
		return;
	}
 
	// 准备遍历所有设备查找USB
	SP_DEVINFO_DATA sDevInfoData;
	sDevInfoData.cbSize = sizeof(SP_DEVINFO_DATA);
 
	TCHAR szDIS[MAX_PATH]; // Device Identification Strings, 
	DWORD nSize = 0;
	for (int i = 0; SetupDiEnumDeviceInfo(hDevInfo, i, &sDevInfoData); i++)
	{
		nSize = 0;
		if (!SetupDiGetDeviceInstanceId(hDevInfo, &sDevInfoData, szDIS, sizeof(szDIS), &nSize))
		{
			AfxMessageBox(_T("获取设备识别字符串失败"));
			break;
		}
		// 设备识别串的前三个字符是否是"USB", 模板: USBVID_XXXX&PID_XXXX0000xxxxxxx
		CString strDIS(szDIS);
		strDIS.MakeUpper();
		if (strDIS.Left(3) == _T("USB"))
		{
			int pos = strDIS.ReverseFind('\');
			CString DeviceSer = strDIS.Right(strDIS.GetLength() - pos-1);
			//匹配序列号
			if (ValidDevcie(DeviceSer))
			{
				SetUSBState(TRUE);
			}
		}
	}
	// 释放设备
	SetupDiDestroyDeviceInfoList(hDevInfo);
}

 

第二种:

windows操作系统在检测到硬件变化时,会发送一个WM_DEVICECHANGE硬件change消息。因此,我们要做的就是在我们的程序中添加WM_DEVICECHANGE的消息响应。

BEGIN_MESSAGE_MAP(CHWDetectDlg, CDialog)zai
    // ... other handlers
    ON_MESSAGE(WM_DEVICECHANGE, OnMyDeviceChange)
END_MESSAGE_MAP()
 
LRESULT CHWDetectDlg::OnMyDeviceChange(WPARAM wParam, LPARAM lParam)
{
    // for more information, see MSDN help of WM_DEVICECHANGE
    // this part should not be very difficult to understand
    if ( DBT_DEVICEARRIVAL == wParam || DBT_DEVICEREMOVECOMPLETE == wParam ) {
        PDEV_BROADCAST_HDR pHdr = (PDEV_BROADCAST_HDR)lParam;
        switch( pHdr->dbch_devicetype ) {
            case DBT_DEVTYP_DEVICEINTERFACE:
                PDEV_BROADCAST_DEVICEINTERFACE pDevInf = (PDEV_BROADCAST_DEVICEINTERFACE)pHdr;
                 //根据pDevHnd结构体内的一些数据判断是否为指定设备,例如
                //Str = pDevInf->dbcc_name;
			   //Resul1 = Str.Find("0483");
			    //resul2 = Str.Find("5750");
                 //利用上面获取比较PID与VID方式判断发生改变的设备是否为指定设备
                break;
 
            case DBT_DEVTYP_HANDLE:
                PDEV_BROADCAST_HANDLE pDevHnd = (PDEV_BROADCAST_HANDLE)pHdr;
               
                // do something...
                break;
 
            case DBT_DEVTYP_OEM:
                PDEV_BROADCAST_OEM pDevOem = (PDEV_BROADCAST_OEM)pHdr;
                // do something...
                break;
 
            case DBT_DEVTYP_PORT:
                PDEV_BROADCAST_PORT pDevPort = (PDEV_BROADCAST_PORT)pHdr;
                // do something...
                break;
 
            case DBT_DEVTYP_VOLUME:
                PDEV_BROADCAST_VOLUME pDevVolume = (PDEV_BROADCAST_VOLUME)pHdr;
                // do something...
                break;
        }
    }
    return 0;
}

然而默认情况下,Windows操作系统发送WM_DEVICECHANGE有些限制:

1 只有顶层窗体的程序才能收到这个消息

2 仅仅串口、磁盘发生改变,才对每个程序广播这个消息

的确不错,至少你可以知道移动U盘、移动硬盘、光盘被安装或弹出了,通过DEV_BROADCAST_VOLUME.dbcv_unitmask你也可以获得其对应的盘符。但实际上,你不知道底层处理的是哪个物理设备实际上被安装到了系统中。

API:RegisterDeviceNotification()

所以,你不得不调用RegisterDeviceNotification()API来注册其他类型的设备改变,或是你的程序仅仅是一个服务程序、没有顶层窗体的程序。例如:如下的例子是用来注册一个设备类型的接口的:

void CDataSyncAccessDlg::RegMessage()
{
	HDEVNOTIFY hDevNotify;
	DEV_BROADCAST_DEVICEINTERFACE NotificationFilter;
	ZeroMemory(&NotificationFilter, sizeof(NotificationFilter));
	NotificationFilter.dbcc_size = sizeof(DEV_BROADCAST_DEVICEINTERFACE);
	NotificationFilter.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE;  //需要注册的设备类型
   //下面是枚举了很多设备的dbcc_classguid,这里也可以指定具体某一个
	for (int i = 0; i<sizeof(GUID_DEVINTERFACE_LIST) / sizeof(GUID); i++) {
		NotificationFilter.dbcc_classguid = GUID_DEVINTERFACE_LIST[i]; 
		hDevNotify = RegisterDeviceNotification(this->GetSafeHwnd(), &NotificationFilter, DEVICE_NOTIFY_WINDOW_HANDLE);
		if (!hDevNotify) {
			AfxMessageBox(CString("Can't register device notification: ")
				+ _com_error(GetLastError()).ErrorMessage(), MB_ICONEXCLAMATION);
			return;
		}
	}
}

//DEVICE_NOTIFY_WINDOW_HANDLE 只响应对应dbcc_classguid的设备
//DEVICE_NOTIFY_ALL_INTERFACE_CLASSES 响应所有设备(即所有设备变动都会响应)

 

GUID简介

一个支持即插即用的设备,有2个不同的GUID相关,一个设备接口GUID, 一个是设备类GUID

设备类GUID:定义了广泛意义上一类设备的GUID,如果你打开设备管理器[我的电脑右键—>设备管理器],默认的是按照“类型”排列的,每一个“类型”就是一个设备类,同时每一个设备类有一个唯一的ID就是设备类GUID。设备GUID定义了此类设备的图标、默认的安全设置、安装属性(例如用户不能手动安装这类设备,而必须通过PNP来遍历),以及其他的设置信息。设备类GUID没有定义对应的I/O接口(请参考术语表),而更像是设备的分组。我认为一个比较好的例子是端口类。串口COM和并口LPT 都是端口类的一部分,但其各有各的I/O接口,而且彼此互不兼容.一个设备仅仅属于一个设备类。我们可以通过设备驱动的INF文件的开头来查看该设备的设备类GUID。

设备接口GUID:定义了相互关联I/O接口的GUID,每一个接口GUID的具体实例都支持基本的I/O设置。设备接口GUID也是对应的驱动程序基于PNP状态来注册、启用、禁用设备。如果需要,一个设备甚至可以注册多个同样GUID的实例(假使每个都有相同的名字)[注:在实际的程序中,多次插拔USB口,确实会驱出相同的串口,例如port12,port12,port12…],尽管在现实世界中完全不需要这样。一个简单的I/O关联接口是键盘设备,每个键盘设备的接口GUID必须相同。

可以通过如下的注册表路径来查看当前设备类GUID, 设备接口GUID:

  • \HKEY_LOCAL_MACHINESYSTEMCurrentControlSetControlClass
  • \HKEY_LOCAL_MACHINESYSTEMCurrentControlSetControlDeviceClasses

 常用设备的接口GUID如下:

Device Interface NameGUID
USB Raw Device{a5dcbf10-6530-11d2-901f-00c04fb951ed}
Disk Device{53f56307-b6bf-11d0-94f2-00a0c91efb8b}
Network Card{ad498944-762f-11d0-8dcb-00c04fc3358c}
Human Interface Device (HID){4d1e55b2-f16f-11cf-88cb-001111000030}
Palm{784126bf-4190-11d4-b5c2-00c04f687a67}

从MSDN中,我们知道:

typedef struct _DEV_BROADCAST_DEVICEINTERFACE {
    DWORD dbcc_size;
    DWORD dbcc_devicetype;
    DWORD dbcc_reserved;
    GUID dbcc_classguid;
    TCHAR dbcc_name[1];
} DEV_BROADCAST_DEVICEINTERFACE *PDEV_BROADCAST_DEVICEINTERFACE;

我们似乎可以通过dbcc_name知道那个设备安装到了当前系统。答案是错误的,dbcc_name仅仅是操作系统内部使用来做为ID的,其实不易读的,例如下面的这个dbcc_name:

 

\?USB#Vid_04e8&Pid_503b#0002F9A9828E0F06#{a5dcbf10-6530-11d2-901f-00c04fb951ed}

  • \?USB: USB 意思是这是一个USB设备类
  • Vid_04e8&Pid_053b: Vid/Pid 是硬件ID,由厂商ID和产品ID组成(但这是由设备类指定的,USB设备类使用VID/PID,不同的设备类使用不同的命名约定)
  • 002F9A9828E0F06: 不清楚是怎么生成的,是唯一设备ID
  • {a5dcbf10-6530-11d2-901f-00c04fb951ed}:设备接口类GUID

现在,我们来解出设备描述信息或是设备别名,有2种办法:

1 直接读注册表, \HKLMSYSTEMCurrentControlSetEnumUSBVid_04e8&Pid_503b002F9A9828E0F06

2 使用 SetupDiXxx 系列API

API:SetupDiXxx()

Windows定义了一组API,让用户通过编程的办法来获取对应的硬件设备信息。例如,我们可以通过dbcc_name来获得设备描述信息或是设备别名。下面是这个办法都具体步骤:

1 首先通过SetupDiGetClassDevs()来获得设备信息集 HDEVINFO,这个操作等同于是一个获取目录句柄的过程。

2 接着使用SetupDiEnumDeviceInfo()来遍历出这个设备信息集内的所有设备,这个操作等同于把目录列表的过程。对于每个遍历出的,我们可以获得SP_DEVINFO_DATA,这个等同于是文件句柄。

3 在上面的枚举过程中,使用SetupDiGetDeviceInstanceId()来读取每个设备的实例ID,这个操作等同于是读文件的属性,一个设备的实例ID类似这个:”USBVid_04e8&Pid_503b002F9A9828E0F06”,和dbcc_name非常像。

4 如果设备的实例ID等同于dbcc_name,则通过SetupDiGetDeviceRegistryProperty()来获取设备描述信息或是设备别名信息。

void CHWDetectDlg::UpdateDevice(PDEV_BROADCAST_DEVICEINTERFACE pDevInf, WPARAM wParam)
{
    // dbcc_name:
    // \?USB#Vid_04e8&Pid_503b#0002F9A9828E0F06#{a5dcbf10-6530-11d2-901f-00c04fb951ed}
    // convert to
    // USBVid_04e8&Pid_503b002F9A9828E0F06
    ASSERT(lstrlen(pDevInf->dbcc_name) > 4);
    CString szDevId = pDevInf->dbcc_name+4;
    int idx = szDevId.ReverseFind(_T('#'));
    ASSERT( -1 != idx );
    szDevId.Truncate(idx);
    szDevId.Replace(_T('#'), _T('\'));
    szDevId.MakeUpper();
 
    CString szClass;
    idx = szDevId.Find(_T('\'));
    ASSERT(-1 != idx );
    szClass = szDevId.Left(idx);
 
    // if we are adding device, we only need present devices
    // otherwise, we need all devices
    DWORD dwFlag = DBT_DEVICEARRIVAL != wParam
        ? DIGCF_ALLCLASSES : (DIGCF_ALLCLASSES | DIGCF_PRESENT);
    HDEVINFO hDevInfo = SetupDiGetClassDevs(NULL, szClass, NULL, dwFlag);
    if( INVALID_HANDLE_VALUE == hDevInfo )
    {
        AfxMessageBox(CString("SetupDiGetClassDevs(): ")
            + _com_error(GetLastError()).ErrorMessage(), MB_ICONEXCLAMATION);
        return;
    }
 
    SP_DEVINFO_DATA* pspDevInfoData =
        (SP_DEVINFO_DATA*)HeapAlloc(GetProcessHeap(), 0, sizeof(SP_DEVINFO_DATA));
    pspDevInfoData->cbSize = sizeof(SP_DEVINFO_DATA);
    for(int i=0; SetupDiEnumDeviceInfo(hDevInfo,i,pspDevInfoData); i++)
    {
        DWORD DataT ;
        DWORD nSize=0 ;
        TCHAR buf[MAX_PATH];
 
        if ( !SetupDiGetDeviceInstanceId(hDevInfo, pspDevInfoData, buf, sizeof(buf), &nSize) )
        {
            AfxMessageBox(CString("SetupDiGetDeviceInstanceId(): ")
                + _com_error(GetLastError()).ErrorMessage(), MB_ICONEXCLAMATION);
            break;
        }
 
        if ( szDevId == buf )
        {
            // device found
            if ( SetupDiGetDeviceRegistryProperty(hDevInfo, pspDevInfoData,
                SPDRP_FRIENDLYNAME, &DataT, (PBYTE)buf, sizeof(buf), &nSize) ) {
                // do nothing
            } else if ( SetupDiGetDeviceRegistryProperty(hDevInfo, pspDevInfoData,
                SPDRP_DEVICEDESC, &DataT, (PBYTE)buf, sizeof(buf), &nSize) ) {
                // do nothing
            } else {
                lstrcpy(buf, _T("Unknown"));
            }
            // update UI
            // .....
            // .....
            break;
        }
    }
 
    if ( pspDevInfoData ) HeapFree(GetProcessHeap(), 0, pspDevInfoData);
    SetupDiDestroyDeviceInfoList(hDevInfo);
}
 

禁用设备

假使你有一个正确的HDEVINFO和SP_DEVINFO_DATA(实际上,我们保持dbcc_name座位树节点的tag,当右键单击某一个节点的时候,可以通过调用SetupDiGetClassDevs和SetupDiEnumDeviceInfo来获得所需东西),按照如下的步骤即可禁用一个设备:

1 给SP_PROPCHANGE_PARAMS结构体赋上正确的值

2 把上面赋完值的SP_PROPCHANGE_PARAMS作为参数传入到SetupDiSetClassInstallParams()

3 调用SetupDiCallClassInstaller(),传递参数DIF_PROPEFRTYCHANGE

实际上,DIF也是按位做与运算后兼容的,你也可以去传递不同的DIF参数来调用SetupDiSetClassInstallParams()。 更多信息,请参考MSDN”Handling DIF Codes”

SP_PROPCHANGE_PARAMS spPropChangeParams ;
spPropChangeParams.ClassInstallHeader.cbSize = sizeof(SP_CLASSINSTALL_HEADER);
spPropChangeParams.ClassInstallHeader.InstallFunction = DIF_PROPERTYCHANGE ;
spPropChangeParams.Scope = DICS_FLAG_GLOBAL ;
spPropChangeParams.HwProfile = 0; // current hardware profile
spPropChangeParams.StateChange = DICS_DISABLE
 
if( !SetupDiSetClassInstallParams(hDevInfo, &spDevInfoData,
    // note we pass spPropChangeParams as SP_CLASSINSTALL_HEADER
    // but set the size as sizeof(SP_PROPCHANGE_PARAMS)
    (SP_CLASSINSTALL_HEADER*)&spPropChangeParams, sizeof(SP_PROPCHANGE_PARAMS)) )
{
    // handle error
}
else if(!SetupDiCallClassInstaller(DIF_PROPERTYCHANGE, hDevInfo, &spDevInfoData))
{
    // handle error
}
else
{
    // ok, show disable success dialog
    // note, after that, the OS will post DBT_DEVICEREMOVECOMPLETE for the disabled device
}

附录:

我使用这个程序,已经多次测试了USB的无线网卡的,插入、拔出测试。

局限性:

1 明显的,必须先运行该程序,才能检测硬件设备。例如:设备在操作系统启动前就已经连接,或者在这个程序运行前的连接都不会被检测。但这个问题,可以通过保存当前系统配置到远程计算机上,等启动完这个程序后再坚持不同的配置来解决

2 我们可以禁用设备,换而言之这也是我们所有能做到。我们不能访问设备底层控制。 我认为可以通过重新用基于内核的过滤驱动来实现,则可以解决这个问题。

非常好的文章

原文:http://www.codeproject.com/Articles/14500/Detecting-Hardware-Insertion-and-or-Removal

一个封装的库http://www.codeproject.com/Articles/119168/Hardware-Change-Detection

最后

以上就是称心大树为你收集整理的USB 设备热插拔的检测的全部内容,希望文章能够帮你解决USB 设备热插拔的检测所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(47)

评论列表共有 0 条评论

立即
投稿
返回
顶部