《HarmonyOS技术精讲-Basic Services Kit基础服务》USB服务设备发现与通信USB服务是HarmonyOS Basic Services Kit中经常被误解的一个模块。很多人以为接上U盘就能直接用fileIO读文件但应用沙箱机制并不允许直接访问外部存储。真正能用的方案是通过USB服务自行实现大容量存储协议——也就是手动发送SCSI命令把U盘当成一个“黑盒设备”来读写扇区。这个例子本身能跑但放到实际项目里还需要处理很多细节。本文将从设备枚举、权限请求、控制传输到批量传输一步步实现检测U盘并读取根目录文件列表的功能。它解决了什么问题USB大容量存储设备U盘、移动硬盘在HarmonyOS设备上可以通过USB Host模式直接通信。系统不提供“挂载成文件系统再读文件”的途径所以必须自己实现SCSI命令和文件系统解析。适用场景OTG U盘数据采集如读取传感器记录文件嵌入式设备固件升级定制化文件管理工具不适用场景有更好方案简单的文件复制建议用文件选择器Picker让用户主动选择文件打印机或键盘有专门的服务Kit环境说明DevEco Studio 版本DevEco Studio 6.1.0 及以上 HarmonyOS SDK 版本HarmonyOS 6.1.0(23) 及以上 目标设备手机支持USB OTG核心实现从枚举到读取文件列表第一步添加权限与导入模块在entry/src/main/module.json5中添加USB权限{module:{requestPermissions:[{name:ohos.permission.USB,reason:需要访问USB设备}]}}在代码中导入usbManagerimport{usbManager}fromkit.BasicServicesKit;第二步枚举USB设备并筛选存储设备usbManager.getDevices()返回所有已连接的USB设备。我们需要根据bInterfaceClass 0x08Mass Storage过滤。functiongetMassStorageDevices():ArrayusbManager.USBDevice{constdevicesusbManager.getDevices();conststorageDevices:ArrayusbManager.USBDevice[];for(constdeviceofdevices){for(constconfigofdevice.configs){for(constifaceofconfig.interfaces){if(iface.bInterfaceClass0x08){storageDevices.push(device);break;}}}}returnstorageDevices;}这段代码遍历每个设备的配置描述符找到接口类为Mass Storage的U盘设备。注意一个设备可能包含多个配置通常取第一个即可。第三步请求设备权限只有获取权限后才能与设备通信。requestDevicePermission会弹出系统弹窗asyncfunctionrequestUsbPermission(device:usbManager.USBDevice):Promisevoid{try{awaitusbManager.requestDevicePermission(device.deviceId);console.info(权限获取成功);}catch(error){console.error(权限获取失败JSON.stringify(error));throwerror;}}权限是一次性的应用被杀死后需要重新请求。第四步声明接口claimInterface声明接口后才能进行通信。通常在claimInterface后系统会锁定该接口其他应用无法同时使用。functionclaimMassStorageInterface(device:usbManager.USBDevice):usbManager.USBInterface|undefined{for(constconfigofdevice.configs){for(constifaceofconfig.interfaces){if(iface.bInterfaceClass0x08){usbManager.claimInterface(device.deviceId,iface.id,true);returniface;}}}returnundefined;}第二个参数force设为true表示强制声明如果接口已被其他应用使用会失败。实际项目中建议先检查再声明。第五步获取端点地址存储设备通常有一个批量输入端点设备到主机和一个批量输出端点主机到设备。需要从接口描述符中提取interfaceEndpointInfo{bulkInEndpoint:number;bulkOutEndpoint:number;}functiongetBulkEndpoints(iface:usbManager.USBInterface):EndpointInfo{letbulkIn-1;letbulkOut-1;for(constendpointofiface.endpoints){constdirection(endpoint.bEndpointAddress0x80)7;consttransferTypeendpoint.bmAttributes0x03;if(transferType0x02){// Bulkif(direction1){bulkInendpoint.bEndpointAddress;}else{bulkOutendpoint.bEndpointAddress;}}}if(bulkIn-1||bulkOut-1){thrownewError(未找到批量端点);}return{bulkInEndpoint:bulkIn,bulkOutEndpoint:bulkOut};}bEndpointAddress的低7位是端点号最高位表示方向1输入。第六步控制传输获取设备描述符可选虽然不通过控制传输也能通信但可以用来验证设备连接状态。这里演示获取设备描述符中的厂商ID和产品IDasyncfunctiongetDeviceDescriptor(device:usbManager.USBDevice):PromiseUint8Array{constbuffernewUint8Array(18);// 标准设备描述符长度18字节constresultusbManager.controlTransfer(device.deviceId,0x80,// bmRequestType: 设备到主机0x06,// bRequest: GET_DESCRIPTOR0x0100,// wValue: 设备描述符类型(1) 8 | 索引00x0000,// wIndex: 语言ID (0)buffer);if(result0){thrownewError(控制传输失败);}returnbuffer;}controlTransfer的返回值是实际传输的字节数。注意wValue高字节为描述符类型低字节为索引。第七步批量传输——发送SCSI INQUIRY命令SCSI命令通过批量输出端点发送CBWCommand Block Wrapper然后从批量输入端点接收CSWCommand Status Wrapper和数据。我们封装一个通用函数发送SCSI命令并接收响应interfaceSCSIResponse{status:number;// 0成功data:Uint8Array;}asyncfunctionsendSCSICommand(device:usbManager.USBDevice,endpointInfo:EndpointInfo,cdb:Uint8Array,// 命令描述块如INQUIRY的CDB为{0x12, 0, 0, 0, 36, 0}dataLength:number,direction:in|out):PromiseSCSIResponse{// 构建CBWconstcbwLength31;constcbwnewUint8Array(cbwLength);// CBW签名0x43425355cbw[0]0x55;cbw[1]0x53;cbw[2]0x42;cbw[3]0x43;// dCBWTag随便一个值cbw[4]0x01;cbw[5]0x00;cbw[6]0x00;cbw[7]0x00;// dCBWDataTransferLength小端constviewnewDataView(cbw.buffer);view.setUint32(8,dataLength,true);// bmCBWFlags0x00表示主机到设备0x80表示设备到主机cbw[12]directionin?0x80:0x00;// bCBWLUN通常0cbw[13]0;// bCBWCBLengthCDB长度cbw[14]cdb.length;// 填充CDBfor(leti0;icdb.length;i){cbw[15i]cdb[i];}// 发送CBW到批量输出端点usbManager.bulkTransfer(device.deviceId,endpointInfo.bulkOutEndpoint,cbw,5000);letdata:Uint8Array|undefined;if(dataLength0){if(directionin){constbufnewUint8Array(dataLength);constresultusbManager.bulkTransfer(device.deviceId,endpointInfo.bulkInEndpoint,buf,5000);databuf.slice(0,result);}else{// 写入数据到设备}}// 读取CSW13字节constcswnewUint8Array(13);constcswResultusbManager.bulkTransfer(device.deviceId,endpointInfo.bulkInEndpoint,csw,5000);if(cswResult13){thrownewError(CSW读取失败);}conststatuscsw[12];// bCSWStatus: 0成功, 1失败, 2阶段错误return{status,data:data??newUint8Array(0)};}注意bulkTransfer的返回值是实际传输字节数非负表示成功。如果超时或出错会抛异常。第八步读取设备信息INQUIRY与READ CAPACITY使用INQUIRY命令获取设备型号字符串asyncfunctionscsiInquiry(device:usbManager.USBDevice,endpoints:EndpointInfo):Promisestring{constcdbnewUint8Array([0x12,0x00,0x00,0x00,0x24,0x00]);constresponseawaitsendSCSICommand(device,endpoints,cdb,36,in);if(response.status!0){thrownewError(INQUIRY命令失败);}// 提取厂商信息8字节和产品信息16字节constvendorString.fromCharCode(...response.data.slice(8,16)).trim();constproductString.fromCharCode(...response.data.slice(16,32)).trim();return${vendor}${product};}asyncfunctionscsiReadCapacity(device:usbManager.USBDevice,endpoints:EndpointInfo):Promisenumber{constcdbnewUint8Array([0x25,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00]);constresponseawaitsendSCSICommand(device,endpoints,cdb,8,in);if(response.status!0){thrownewError(READ CAPACITY失败);}// 返回总扇区数大端constviewnewDataView(response.data.buffer);constblocksview.getUint32(0,false);constblockSizeview.getUint32(4,false);returnblocks*blockSize;}第九步读取根目录扇区并解析文件名这是一个“足够简单”的解析假定U盘是FAT32格式。我们读取第一个扇区LBA 0判断是否为MBR然后找到第一个分区再读取分区的引导扇区获取根目录信息。实际代码会很长这里给出关键函数asyncfunctionreadSector(device:usbManager.USBDevice,endpoints:EndpointInfo,lba:number,sectorSize:number):PromiseUint8Array{constcdbnewUint8Array(10);cdb[0]0x28;// READ10cdb[2](lba24)0xFF;cdb[3](lba16)0xFF;cdb[4](lba8)0xFF;cdb[5]lba0xFF;cdb[7](sectorSize8)0xFF;cdb[8]sectorSize0xFF;constresponseawaitsendSCSICommand(device,endpoints,cdb,sectorSize*512,in);if(response.status!0){thrownewError(READ10失败);}returnresponse.data;}functionparseFAT32RootDirectory(sectorData:Uint8Array):string[]{constfiles:string[][];// 每个目录项32字节for(leti0;isectorData.length;i32){if(sectorData[i]0x00)break;// 空条目结束if(sectorData[i]0xE5)continue;// 已删除constattrsectorData[i11];if(attr0x08)continue;// 卷标// 提取文件名8.3格式letname;for(letj0;j8;j){constcsectorData[ij];if(c0x20)break;nameString.fromCharCode(c);}constext[];for(letj8;j11;j){constcsectorData[ij];if(c0x20)break;ext.push(String.fromCharCode(c));}if(ext.length0)name.ext.join();files.push(name);}returnfiles;}