☰
UWP 自定义 USB 设备访问实战:Windows.Devices.Usb 驱动的 7 大通信场景与驱动配置全解析
2026/9/26 1:15:09 网站建设 项目流程
  • 示例工程

【免费下载链接】Windows-universal-samples

API samples for the Universal Windows Platform.

项目地址:https://gitcode.com/gh_mirrors/wi/Windows-universal-samples
点击查看免费下载

本指南以 Windows-universal-samples 仓库中的 Custom USB Device Access 示例(原文档,JavaScript 实现位于 archived/CustomUsbDeviceAccess/js)为核心,系统讲解如何在 UWP 应用中通过Windows.Devices.Usb命名空间与 WinUSB 设备(Winusb.sys 驱动)完成设备发现、控制/中断/批量传输、描述符解析、接口设置切换与后台同步。读完本文,你将掌握 USB 设备从清单声明、驱动安装到数据收发的完整链路,并可将示例快速适配到自己的 USB 硬件。

示例概览:它能做什么,支持哪些设备

Custom USB Device Access 示例展示了 UWP 应用通过 Windows.Devices.Usb 命名空间与外部 USB 设备通信的完整过程。该命名空间提供了一套 Windows Runtime 类与枚举,专门面向以 WinUSB(Winusb.sys)作为设备驱动的 USB 外设,无需编写内核驱动即可在应用层完成传输。

示例内置了对两类硬件的支持:

  • OSR USB FX2 学习套件(OSRFX2):来自 OSR Online 的经典 USB 学习板,具备七段数码管、拨码开关等可演示器件;
  • SuperMUTT 设备:来自 JJG Technologies 的 USB 测试设备,使用前必须先用 MUTT 软件包更新固件并切换到 WinRTUsbPersonality 模式(下文详述)。

示例演示的关键场景覆盖了 USB 设备编程的几乎所有基础动作:

  1. 如何连接(打开)一个 USB 设备;
  2. 如何发送 USB 控制传输(vendor 命令);
  3. 如何发送/接收 USB 中断传输;
  4. 如何发送 USB 批量传输;
  5. 如何获取 USB 描述符(设备、配置、接口、端点、原始描述符);
  6. 如何枚举并选择 USB 接口的备用设置(alternate interface setting);
  7. 如何处理应用挂起(suspending)与恢复(resuming)事件;
  8. 如何在后台任务中与设备同步数据。

运行前置条件

操作系统要求

  • 客户端:Windows 10
  • 服务器:Windows Server 2016 Technical Preview

示例工程(CustomUsbDeviceAccess.sln、CustomUsbDeviceAccess.jsproj)的清单中,TargetDeviceFamily指定MinVersion="10.0.10240.0"、MaxVersionTested="10.0.18362.0"(见 Package.appxmanifest),表明该示例面向 Windows 10 通用平台。

驱动要求:Winusb.sys

示例应用本身不包含任何驱动,它通过微软提供的内核模式驱动Winusb.sys与设备通信,因此你必须先将其安装为设备驱动。硬件厂商通常通过以下两种方式之一让系统自动完成安装:

  • 提供自定义 INF:厂商编写一个引用微软 Winusb.inf 的定制 INF 文件,Windows 据此加载 Winusb.sys;
  • 声明微软 OS 特征描述符(OS feature descriptors):设备固件中报告兼容 ID(compatible ID)为"WINUSB",Windows 匹配到该兼容 ID 后自动加载 Winusb.sys 作为设备驱动。

如果插入设备后 Windows 没有自动加载 Winusb.sys,可按下述步骤手动安装:

  1. 打开设备管理器(Device Manager),定位到该设备;
  2. 右键点击设备,在上下文菜单中选择更新驱动程序软件...(Update driver software...);
  3. 在向导中选择浏览计算机以查找驱动程序软件(Browse my computer for driver software);
  4. 选择从计算机的设备驱动程序列表中选择(Let me pick from a list of device drivers on my computer);
  5. 在设备类别列表中选择Universal Serial Bus devices;
  6. 向导会显示WinUsb Device,选中它即可完成驱动加载。

注意:以上步骤不会为 OSRFX2 添加设备接口 GUID,OSRFX2 与 SuperMUTT 还有各自的额外准备工作(见下两节)。

设备准备:OSRFX2 与 SuperMUTT 的差异化配置

如果使用 OSRFX2:手动添加设备接口 GUID

OSRFX2 走的是自定义 INF 方案,手动加载 Winusb.sys 后,系统不会自动为其创建设备接口 GUID,应用无法直接定位设备,必须手工补一条注册表项:

  1. 按上文步骤加载驱动;

  2. 使用 guidgen.exe 之类的工具为 OSRFX2 生成一个设备接口 GUID;

  3. 在注册表中找到 OSRFX2 设备对应的键(本例 VID/PID 为 VID_0547 & PID_1002):

    HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum\USB\VID_0547&PID_1002

  4. 在该键下的Device Parameters子键中,新增名为DeviceInterfaceGUID的字符串值(String),或名为DeviceInterfaceGUIDs的多字符串值(Multi-String),将值设置为第 2 步生成的 GUID;

  5. 断开设备并重新插入同一个物理端口。

注意:如果更换了物理端口,必须重复步骤 1~4。

如果使用 SuperMUTT:固件更新与模式切换

SuperMUTT 通常由 Windows 自动加载 Winusb.sys(否则按上文手动安装)。除此之外还需完成固件准备:

  1. 下载并安装MUTT Software Package;

  2. 打开命令提示符,使用包内附带的 MuttUtil 工具更新固件:

    MuttUtil.exe -forceupdatefirmware

  3. 继续使用 MuttUtil 将设备切换到 WinRTUsbPersonality 模式:

    MuttUtil.exe -SetWinRTUsb

只有把 SuperMUTT 配置为 WinRTUsbPersonality 模式后,设备暴露出的配置、接口和端点才能与示例中的各场景配合使用。

应用清单声明:DeviceCapability 与后台任务

UWP 应用要访问 USB 设备,必须在 Package.appxmanifest 中声明<DeviceCapability Name="usb">,并列出设备信息——包括厂商/产品 ID(VID/PID)以及设备类别信息。示例清单中的声明如下:

<Capabilities> <!--当设备的 classId 为 FF * * 时,类别有预定义名称,可用名称代替 classId;还有其他预定义名称对应不同 classId--> <DeviceCapability Name="usb"> <!--MUTT Device--> <Device Id="vidpid:045E 078E"> <Function Type="classId:ff * *"/> </Device> <!--OSRFX2 Device--> <Device Id="vidpid:0547 1002"> <Function Type="classId:ff * *"/> </Device> <!--SuperMutt Device--> <Device Id="vidpid:045E 0611"> <Function Type="name:vendorSpecific"/> </Device> </DeviceCapability> </Capabilities>

要点说明:

  • <Device Id="vidpid:XXXX YYYY">以 VID/PID 精确定位设备,其中0547 1002是 OSRFX2,045E 0611是 SuperMUTT,045E 078E是标准 MUTT 测试设备;
  • <Function Type>描述设备所支持的类别:当设备的类别码为FF * *(厂商自定义类)时,既可以使用预定义的classId:ff * *,也可以使用名称name:vendorSpecific代替;
  • 若想用其它方式(如设备类、接口类 GUID)声明能力,需确认设备类在系统中有对应的预定义名称,可参考微软文档中"如何向应用清单添加 USB 设备功能"一节。

此外,示例的 Package.appxmanifest 还声明了一个windows.backgroundTasks扩展,用于后台同步场景(详见 Scenario7):

<Extensions> <Extension Category="windows.backgroundTasks" StartPage="js\ioSyncBackgroundTask.js"> <BackgroundTasks> <Task Type="deviceUse" /> </BackgroundTasks> </Extension> </Extensions>

代码结构总览

示例的 JavaScript 工程按"页面场景 + 共享服务类"组织,核心模块如下:

模块文件职责
App应用入口处理激活事件;当设备接入系统时拉起应用,并向用户展示是哪个设备启动了应用
DeviceListEntryjs/deviceListEntry.js包装DeviceInformation对象,供 UI 绑定显示设备列表(如获取设备接口路径、实例 ID)
EventHandlerForDevicejs/eventHandlerForDevice.js集中管理应用事件(挂起/恢复)与DeviceWatcher事件(添加/移除/访问权限变化),维护当前UsbDevice引用
Scenario1_DeviceConnectjs/scenario1_connectDisconnect.js动态发现并连接设备
Scenario2_ControlTransferjs/scenario2_controlTransfer.js控制传输(vendor 命令)
Scenario3_InterruptPipesjs/scenario3_interruptPipes.js中断 IN/OUT 管道读写
Scenario4_BulkPipesjs/scenario4_bulkPipes.js批量 IN/OUT 管道读写与异步取消
Scenario5_UsbDescriptorsjs/scenario5_usbDescriptors.js枚举并展示各类描述符
Scenario6_InterfaceSettingsjs/scenario6_interfaceSettings.js枚举/选择接口备用设置
Scenario7_SyncDevicejs/scenario7_syncDevice.js、js/ioSyncBackgroundTask.js后台任务与设备同步

EventHandlerForDevice:设备生命周期与事件中枢

eventHandlerForDevice.js 是整个示例的基石,它以 WinJS 单例(EventHandlerForDeviceClass.current)形式存在——因为示例同一时刻只与一台设备通信,设备句柄必须跨场景页面存活。

打开与关闭设备

核心方法是openDeviceAsync(L121-L191),它调用Windows.Devices.Usb.UsbDevice.fromIdAsync(deviceInfo.id)打开设备句柄并保存到_device字段。若打开失败(返回 null),代码会通过DeviceAccessInformation.createFromId(...).currentStatus区分失败原因:

  • deniedByUser:用户在设置中屏蔽了设备访问;
  • deniedBySystem:多半是应用未在 Package.appxmanifest 中声明设备能力;
  • 其它情况:大概率是设备已被另一个应用独占打开。

closeDevice(L196-L227)则完整地关闭设备、停止DeviceWatcher、注销各类事件监听并重置对象状态。

从源码注释可以看出关闭语义:当UsbDevice关闭时,系统会取消所有仍在进行中的 IO 操作,但不会等待 IO 完成回调,因此关闭调用可能先于回调返回。

应用挂起/恢复处理

Windows 可能随时挂起、恢复或终止应用。示例在_registerForAppEvents(L257-L262)中注册了WebUIApplication的suspending与resuming事件:

  • 挂起时(_onAppSuspension,L324-L340):
    • 停止所有DeviceWatcher——否则挂起期间 watcher 仍会持续触发事件、消耗电量;
    • 先回调注册的挂起回调(由当前场景提供,用于在关闭设备前保存状态、串行化事件);
    • 显式关闭UsbDevice。原因在于 API 在应用挂起时会自动释放UsbDevice对象,若应用保留陈旧引用,恢复后继续使用会出错;"每一次 open 都应有一次 close",显式关闭是更规范的做法。
  • 恢复时(_onAppResume,L349-L355):重启之前挂起的DeviceWatcher,设备被重新枚举后,若开启了自动重连(isEnabledAutoReconnect),会自动重新打开设备并获得新的UsbDevice引用。

DeviceWatcher 事件与意外拔插

示例注册了DeviceWatcher的added、removed事件(L273-L280):

  • _onDeviceRemoved(L360-L366):检测到当前连接的设备被拔出(包括"惊扰移除"surprise remove)时,显式关闭设备、释放引用;
  • _onDeviceAdded(L371-L383):设备重新插入且isEnabledAutoReconnect为 true 时,自动重新调用openDeviceAsync;若重连失败则关闭自动重连,避免反复尝试。

此外,类还通过DeviceAccessInformation.accesschanged事件(L288-L292、L388-L403)监听权限变化:用户/系统拒绝访问时关闭设备,恢复allowed时按需重连。注意当该事件触发时,系统可能已经自动关闭了设备句柄,显式关闭仍然必要,以便及时清理资源。

Scenario1:动态发现并连接 USB 设备

scenario1_connectDisconnect.js 展示了如何用DeviceWatcher动态检测设备——相比一次性枚举,DeviceWatcher能感知设备的随时插入与移除。

使用 AQS 选择器锁定目标设备

示例分别为两类设备构建了 AQS(Advanced Query Syntax)选择器(L100-L124):

// OSRFX2:通过 VID/PID 定位 var osrFx2Selector = Windows.Devices.Usb.UsbDevice.getDeviceSelector( SdkSample.Constants.osrFx2.deviceVid, SdkSample.Constants.osrFx2.devicePid); // SuperMUTT:通过 VID/PID + 接口类 GUID 定位 var superMuttSelector = Windows.Devices.Usb.UsbDevice.getDeviceSelector( SdkSample.Constants.superMutt.deviceVid, SdkSample.Constants.superMutt.devicePid, SdkSample.Constants.superMutt.deviceInterfaceClass);

UsbDevice.getDeviceSelector还有第三种重载——只按设备类(UsbDeviceClasses)选择设备。这些重载统一返回 AQS 字符串,可直接传入DeviceWatcher.createWatcher()或DeviceInformation.createFromIdAsync()。

DeviceInformation.createWatcher(selector, [])的第二个参数必须传入一个数组(JS 引擎按参数个数区分重载),示例中即以此创建 watcher 并监听added、removed、enumerationcompleted三个事件(L130-L142),将新增设备包装为DeviceListEntry加入 UI 列表,删除时同步移除。连接成功后通过eventHandlerForDevice.current.openDeviceAsync(...)打开设备。

UI 上"断开连接"按钮的语义也值得一提:点击断开时,示例先将isEnabledAutoReconnect置为 false 再closeDevice(),表示这是用户主动断开而非等待自动重连;按钮文案会在"Disconnect from device"与"Do not automatically reconnect to device that was just closed"之间切换。

Scenario2:USB 控制传输(vendor 命令)

scenario2_controlTransfer.js 演示了如何构造 USB 设置包(setup packet)并发送控制传输。控制传输由三阶段组成:设置阶段(Setup)、可选的数据阶段(Data)、状态阶段(Status)。示例完全面向 vendor 类命令,对应两个 API:

  • UsbDevice.sendControlOutTransferAsync(setupPacket, buffer):向设备发送数据;
  • UsbDevice.sendControlInTransferAsync(setupPacket, buffer):从设备读取数据。

构造 UsbSetupPacket

以设置 OSRFX2 七段数码管为例(L25-L51),完整的 setup packet 构造如下:

var setupPacket = new Windows.Devices.Usb.UsbSetupPacket(); setupPacket.requestType.recipient = Windows.Devices.Usb.UsbControlRecipient.device; setupPacket.requestType.controlTransferType = Windows.Devices.Usb.UsbControlTransferType.vendor; setupPacket.request = SdkSample.Constants.osrFx2.vendorCommand.setSevenSegment; // 0xDB setupPacket.value = 0; setupPacket.length = bufferToSend.length;

对应的设备端协议为:bmRequestType(类型=VENDOR,接收方=DEVICE)、bRequest=0xDB、wLength=1。数码管显示值通过查找sevenLedSegmentMask表映射为十六进制位掩码后写入 buffer,每次位段对应一个 LED。读取当前显示值则使用bRequest=0xD4、wLength=1,收到 1 字节后与掩码表比对还原数字(L72-L106)。

复用读取封装

示例把"vendor 类型、DEVICE 接收方的控制读"抽成了公共方法sendVendorControlTransferInToDeviceRecipientAsync(L191-L202):

function sendVendorControlTransferInToDeviceRecipientAsync(vendorCommand, dataPacketLength) { var buffer = new Windows.Storage.Streams.Buffer(dataPacketLength); var setupPacket = new Windows.Devices.Usb.UsbSetupPacket(); setupPacket.requestType.recipient = Windows.Devices.Usb.UsbControlRecipient.device; setupPacket.requestType.controlTransferType = Windows.Devices.Usb.UsbControlTransferType.vendor; setupPacket.request = vendorCommand; setupPacket.length = dataPacketLength; return SdkSample.CustomUsbDeviceAccess.eventHandlerForDevice.current.device.sendControlInTransferAsync(setupPacket, buffer); }

SuperMUTT 的 LED 闪烁模式

设置 SuperMUTT LED 闪烁模式同样走 vendor 控制传输(L129-L141):bRequest=0x03、wValue=0~7、wLength=0。7 种模式含义依次为:0 = LED 常亮;1 = 亮 2 秒、灭 2 秒循环;2 = 亮 2 秒、灭 1 秒、亮 2 秒、灭 4 秒循环;……;7 = 重复 7 次"亮 2 秒、灭 1 秒"后灭 4 秒。

无论读写,都通过DataWriter/DataReader处理IBuffer:DataWriter内部创建缓冲区、writeByte/writeBytes后detachBuffer()取出;读回则用DataReader.fromBuffer(buffer)的readByte()逐字节解析。

Scenario3:USB 中断传输

scenario3_interruptPipes.js 演示中断管道的读写。中断传输适合低延迟、小数据量、由设备主动上报的场景(如开关状态、按键事件)。

注册中断 IN 事件

中断 IN 管道的读取采用事件驱动模型。示例遍历device.defaultInterface.interruptInPipes集合,对指定管道注册datareceived事件(L30-L47):

function registerForInterruptEvent(pipeIndex, eventHandler) { var interruptInPipes = eventHandlerForDevice.current.device.defaultInterface.interruptInPipes; var interruptInPipe = interruptInPipes.getAt(pipeIndex); interruptInPipe.addEventListener("datareceived", eventHandler, false); ... }

注意pipeIndex是interruptInPipes集合中的下标,而非端点号(endpoint number);每个管道通过endpointDescriptor属性暴露端点信息(端点号、最大包大小、轮询间隔 interval)。

  • OSRFX2 场景:事件处理器onOsrFx2SwitchStateChangeEvent(L101-L146)读取eventArgs.interruptData中的 1 字节,逐位解析 8 个拨码开关的状态,并对比上一状态,在表格中将变化的开关加粗显示;
  • SuperMUTT 场景:onGeneralInterruptEvent只统计中断次数与累计字节数。

取消注册时调用removeEventListener并复位内部状态(unregisterForInterruptEvent,L51-L64)。

中断 OUT 写入

中断 OUT 只适用于 SuperMUTT(OSRFX2 没有中断 OUT 端点)。writeToInterruptOut(L70-L92)取interruptOutPipes.getAt(pipeIndex).outputStream,用DataWriter写入数据后调用storeAsync()真正把数据冲到设备端。每次写入量取endpointDescriptor.maxPacketSize(单次中断传输允许的最大字节数)。

Scenario4:USB 批量传输

scenario4_bulkPipes.js 演示批量管道的异步读写与取消。批量传输吞吐量大,适合大块数据传输(SuperMUTT 与 OSRFX2 的批量 IN/OUT 管道都位于索引 0)。

批量读与批量写

// 批量读:DataReader 挂在 bulkInPipes[0].inputStream 上 function bulkReadAsync(bulkPipeIndex, bytesToRead) { var stream = eventHandlerForDevice.current.device.defaultInterface.bulkInPipes.getAt(bulkPipeIndex).inputStream; var reader = new Windows.Storage.Streams.DataReader(stream); bulkPipes.readingPromise = reader.loadAsync(bytesToRead).then(function (bytesRead) { bulkPipes.totalBytesRead += bytesRead; reader.close(); }); return bulkPipes.readingPromise; } // 批量写:DataWriter 挂在 bulkOutPipes[0].outputStream 上 function bulkWriteAsync(bulkPipeIndex, bytesToWrite) { var stream = eventHandlerForDevice.current.device.defaultInterface.bulkOutPipes.getAt(bulkPipeIndex).outputStream; var writer = new Windows.Storage.Streams.DataWriter(stream); writer.writeBytes(new Array(bytesToWrite)); // 示例设备返回/接收的是垃圾数据 bulkPipes.writingPromise = writer.storeAsync().then(function (bytesWritten) { bulkPipes.totalBytesWritten += bytesWritten; writer.close(); }); return bulkPipes.writingPromise; }

UI 每次以 512 字节为一批进行读/写(L234-L274),完成回调中把累计字节数打印出来。读写期间禁用相关按钮,防止并发操作。

循环读写与异步取消

bulkReadWriteAsync(L122-L168)同时发起一个读循环和一个写循环:每次读/写成功后立即发起下一次,直到任务被取消。示例特意强调:取消任务是停止挂起 IO 操作的唯一异步手段,cancelAllIoTasks(L22-L38)保存了readingPromise、writingPromise、readingWritingPromise三个 Promise 引用并逐个调用cancel();若直接关闭UsbDevice,析构时也会取消所有挂起的 IO,但那是隐式行为。

应用挂起前(onAppSuspension回调,L172-L175)也会调用cancelAllIoTasks,因为挂起时设备会被关闭,必须先取消挂起的 IO。Promise 的error.name === "Canceled"分支用于识别"用户主动取消"这一正常路径。

Scenario5:获取 USB 描述符

scenario5_usbDescriptors.js 将设备相关的各类描述符以可读文本输出,UI 提供下拉列表选择:

  • Device Descriptor(设备描述符)
  • Configuration Descriptor(配置描述符)
  • All Interface Descriptors(全部接口描述符)
  • All Endpoint Descriptors(全部端点描述符)
  • All Custom Descriptors(全部原始描述符)
  • Product String(产品字符串)

设备描述符与配置描述符

设备描述符(L93-L109)通过device.deviceDescriptor直接读取,包含:bcdUsb(USB 规范版本号)、maxPacketSize0(端点 0 最大包大小)、vendorId、productId、bcdDeviceRevision(设备修订号)、numberOfConfigurations(配置数)。

配置描述符(L110-L126)通过device.configuration.configurationDescriptor读取,包含:接口数量(usbConfiguration.usbInterfaces.size)、configurationValue(配置值)、selfPowered(是否自供电)、remoteWakeup(是否支持远程唤醒)、maxPowerMilliamps(最大功耗,毫安)。

接口与端点描述符

接口描述符(L130-L156)遍历device.configuration.usbInterfaces,取每个接口第一个设置(interfaceSettings[0].interfaceDescriptor)的classCode、subclassCode、protocolCode,并统计该接口已打开的批量/中断 IN/OUT 管道数量。

端点描述符(L157-L209)遍历defaultInterface的四类管道集合,每个管道的endpointDescriptor提供endpointNumber(端点号)、maxPacketSize(最大包大小),中断端点还额外提供interval(轮询间隔,毫秒)。

原始描述符解析

SuperMUTT 与 OSRFX2 都没有自定义描述符,但示例仍演示了如何遍历原始描述符(L222-L258):完整配置描述符中的所有描述符都挂在usbConfiguration.descriptors下,每个UsbDescriptor通过readDescriptorBuffer(buffer)读出字节,再用DataReader按little-endian(USB 规范规定)解析前两个字节——bLength(描述符长度)与bDescriptorType(描述符类型)。代码注释特别提醒:readByte()会消费当前字节,同一行内连续调用可能因求值顺序导致字节错位。

产品字符串(Product String)属于字符串描述符,但该 API 不像其它描述符那样直接暴露字符串,示例采用推荐做法:直接使用DeviceInformation.name获取产品名;若需厂商名(Manufacturer),则要改用Windows.Devices.Enumeration.PnpAPI(PnpObjectType.DeviceContainer,读取System.Devices.Manufacturer属性)。

Scenario6:枚举与选择接口备用设置

scenario6_interfaceSettings.js 演示接口备用设置(alternate interface setting)的操作。同一接口可以有多个备用设置,不同设置对应不同的端点集合/带宽分配。

  • 枚举:页面加载时遍历device.defaultInterface.interfaceSettings,将每个设置填充到下拉列表(L66-L87);
  • 选择:setInterfaceSetting(settingNumber)(L19-L27)对指定下标的interfaceSetting调用selectSettingAsync()。设置下标从 0 开始,0 是默认设置;
  • 查询当前设置:getInterfaceSetting()(L33-L47)遍历所有设置,找到selected === true的那个,读取其interfaceDescriptor.alternateSettingNumber输出。

示例限定"切换设置"按钮只对 SuperMUTT 可用(OSRFX2 只读),并且由于前两个设置对 API 而言完全相同,切换它不会破坏其它场景的可用性。

Scenario7:后台任务与设备同步

scenario7_syncDevice.js 演示用后台任务同步设备数据。前提约束:UsbDevice对象同一时刻只能被一个进程打开,因此应用在启动后台任务前必须关闭自己持有的句柄,任务结束后再重新打开。

前台:注册并启动 DeviceUseTrigger

同步流程如下:

  1. syncWithDeviceAsync(L40-L88)先通过BackgroundTaskBuilder注册后台任务:设置任务名、入口点(js/ioSyncBackgroundTask.js)与触发器DeviceUseTrigger(L124-L137);
  2. _startSyncBackgroundTaskAsync(L109-L120)保存当前DeviceInformation与选择器后调用closeDevice(),再以设备 ID 调用syncBackgroundTaskTrigger.requestAsync(deviceId);
  3. 根据DeviceTriggerResult判断结果:allowed表示允许同步;lowBattery、deniedByUser、deniedBySystem等失败场景下,示例立即用保存的信息重新打开设备,保证应用状态一致;
  4. 任务完成(completed事件,L143-L187)时通过args.checkResult()检查异常,重新openDeviceAsync夺回设备句柄,并从ApplicationData.localSettings中读取后台任务写入的结果(写入总字节数)与状态(完成/取消),最后注销后台任务注册。

取消同步则调用backgroundTaskRegistration.unregister(true)(cancelSyncWithDevice,L25-L34)。

后台:i/o 同步任务实现

ioSyncBackgroundTask.js 是后台任务入口(需 WinJS 4.1 的base.js)。run()(L30-L76)流程:

  1. 通过WebUIBackgroundTaskInstance.current.triggerDetails.deviceId拿到设备 ID;
  2. UsbDevice.fromIdAsync(deviceId)重新打开设备(此时前台句柄已释放),打开成功后进度置为 10;
  3. writeToDeviceAsync(L122-L183)向defaultInterface.bulkOutPipes[0]循环执行若干次批量写,每次storeAsync()后按比例累加backgroundTaskInstance.progress(向上取整,因为 progress 是无符号整数),用于 UI 进度条;
  4. 结果通过ApplicationData.current.localSettings(syncBackgroundTaskResult/syncBackgroundTaskStatus)回传给前台;
  5. 关闭设备并调用close()完成任务——这会触发前台注册的completed事件。

任务还监听canceled事件(onCanceled,L91-L111),逐个取消打开设备与各次写操作的 Promise。

将示例适配到你自己的设备

扩展该示例支持自有硬件,只需两步:

  1. 在 scenario1_connectDisconnect.js 中为你的设备创建DeviceWatcher:参照_initializeOsrFx2DeviceWatcher/_initializeSuperMuttDeviceWatcher的做法,用UsbDevice.getDeviceSelector(...)生成 AQS 选择器(可按 VID/PID、VID/PID+接口类 GUID 或设备类),再DeviceInformation.createWatcher(selector, [])创建 watcher;
  2. 在 Package.appxmanifest 的DeviceCapability(Name="usb")节点下补充你的设备信息:填写 VID/PID 与Function Type。若指定设备类代码,务必确认该设备类受支持(FF * *厂商类与若干预定义名称均有对应支持)。

示例中设备相关的 VID/PID、vendor 命令、端点索引等参数集中在常量文件中统一管理,适配新设备时也建议沿用这种集中配置方式,便于维护。

构建与运行

构建示例

  1. 若下载的是 samples ZIP 压缩包,务必解压整个压缩包,而不仅仅是本示例所在文件夹——示例依赖共享内容(shared 依赖);
  2. 启动 Microsoft Visual Studio 2017,选择文件>打开>项目/解决方案;
  3. 定位到本示例的js子目录,双击 CustomUsbDeviceAccess.sln 打开解决方案;
  4. 按Ctrl+Shift+B(或菜单生成>生成解决方案)编译。

运行示例

构建成功后,在 Visual Studio 中按F5(带调试运行)或Ctrl+F5(不带调试运行)即可启动(也可通过"调试"菜单选择对应选项)。运行前请确认已按前文完成设备驱动安装与固件/注册表准备,否则各场景会提示设备未连接。

相关技术速查

技术/类作用
Windows.Devices.Usb提供 Windows Runtime 类与枚举,供应用与 WinUSB(Winusb.sys)驱动的 USB 设备通信,包括UsbDevice、UsbSetupPacket、UsbConfiguration、UsbInterface、UsbBulkInPipe、UsbInterruptInPipe等
Windows.Devices.Enumeration提供设备发现与通知能力,DeviceWatcher动态枚举设备,应用可感知设备添加/移除/变化
Windows.ApplicationModel.Background允许应用在挂起时通过后台任务继续执行代码(本示例使用DeviceUseTrigger做设备同步)
DeviceWatcher动态枚举设备,初始枚举完成后仍持续推送添加/移除/更新通知
DataReader / DataWriter从输入流读取 / 向输出流写入数据,示例中用于 USB 管道的数据读写与缓冲区解析

以上所有实现细节均可在 archived/CustomUsbDeviceAccess/js/js 目录的源码中逐一验证——从UsbDevice.fromIdAsync打开句柄,到sendControlIn/OutTransferAsync控制传输、datareceived中断事件、loadAsync/storeAsync批量读写、selectSettingAsync接口切换,再到后台任务中的DeviceUseTrigger同步,构成了 UWP 自定义 USB 设备通信的完整参考实现。

  • 示例工程

【免费下载链接】Windows-universal-samples

API samples for the Universal Windows Platform.

项目地址:https://gitcode.com/gh_mirrors/wi/Windows-universal-samples
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询