《HarmonyOS技术精讲-Basic Services Kit》USB服务:设备发现与通信
2026/7/19 0:13:03 网站建设 项目流程

《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设备"}]}}

在代码中导入usbManager

import{usbManager}from'@kit.BasicServicesKit';

第二步:枚举USB设备并筛选存储设备

usbManager.getDevices()返回所有已连接的USB设备。我们需要根据bInterfaceClass == 0x08(Mass Storage)过滤。

functiongetMassStorageDevices():Array<usbManager.USBDevice>{constdevices=usbManager.getDevices();conststorageDevices:Array<usbManager.USBDevice>=[];for(constdeviceofdevices){for(constconfigofdevice.configs){for(constifaceofconfig.interfaces){if(iface.bInterfaceClass===0x08){storageDevices.push(device);break;}}}}returnstorageDevices;}

这段代码遍历每个设备的配置描述符,找到接口类为Mass Storage的U盘设备。注意一个设备可能包含多个配置,通常取第一个即可。

第三步:请求设备权限

只有获取权限后才能与设备通信。requestDevicePermission会弹出系统弹窗:

asyncfunctionrequestUsbPermission(device:usbManager.USBDevice):Promise<void>{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.bInterfaceClass===0x08){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.bEndpointAddress&0x80)>>7;consttransferType=endpoint.bmAttributes&0x03;if(transferType===0x02){// Bulkif(direction===1){bulkIn=endpoint.bEndpointAddress;}else{bulkOut=endpoint.bEndpointAddress;}}}if(bulkIn===-1||bulkOut===-1){thrownewError('未找到批量端点');}return{bulkInEndpoint:bulkIn,bulkOutEndpoint:bulkOut};}

bEndpointAddress的低7位是端点号,最高位表示方向(1=输入)。

第六步:控制传输获取设备描述符(可选)

虽然不通过控制传输也能通信,但可以用来验证设备连接状态。这里演示获取设备描述符中的厂商ID和产品ID:

asyncfunctiongetDeviceDescriptor(device:usbManager.USBDevice):Promise<Uint8Array>{constbuffer=newUint8Array(18);// 标准设备描述符长度18字节constresult=usbManager.controlTransfer(device.deviceId,0x80,// bmRequestType: 设备到主机0x06,// bRequest: GET_DESCRIPTOR0x0100,// wValue: 设备描述符类型(1) << 8 | 索引00x0000,// wIndex: 语言ID (0)buffer);if(result<0){thrownewError('控制传输失败');}returnbuffer;}

controlTransfer的返回值是实际传输的字节数。注意wValue高字节为描述符类型,低字节为索引。

第七步:批量传输——发送SCSI INQUIRY命令

SCSI命令通过批量输出端点发送CBW(Command Block Wrapper),然后从批量输入端点接收CSW(Command 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'):Promise<SCSIResponse>{// 构建CBWconstcbwLength=31;constcbw=newUint8Array(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(小端)constview=newDataView(cbw.buffer);view.setUint32(8,dataLength,true);// bmCBWFlags:0x00表示主机到设备,0x80表示设备到主机cbw[12]=direction==='in'?0x80:0x00;// bCBWLUN:通常0cbw[13]=0;// bCBWCBLength:CDB长度cbw[14]=cdb.length;// 填充CDBfor(leti=0;i<cdb.length;i++){cbw[15+i]=cdb[i];}// 发送CBW到批量输出端点usbManager.bulkTransfer(device.deviceId,endpointInfo.bulkOutEndpoint,cbw,5000);letdata:Uint8Array|undefined;if(dataLength>0){if(direction==='in'){constbuf=newUint8Array(dataLength);constresult=usbManager.bulkTransfer(device.deviceId,endpointInfo.bulkInEndpoint,buf,5000);data=buf.slice(0,result);}else{// 写入数据到设备}}// 读取CSW(13字节)constcsw=newUint8Array(13);constcswResult=usbManager.bulkTransfer(device.deviceId,endpointInfo.bulkInEndpoint,csw,5000);if(cswResult<13){thrownewError('CSW读取失败');}conststatus=csw[12];// bCSWStatus: 0成功, 1失败, 2阶段错误return{status,data:data??newUint8Array(0)};}

注意bulkTransfer的返回值是实际传输字节数,非负表示成功。如果超时或出错会抛异常。

第八步:读取设备信息(INQUIRY与READ CAPACITY)

使用INQUIRY命令获取设备型号字符串:

asyncfunctionscsiInquiry(device:usbManager.USBDevice,endpoints:EndpointInfo):Promise<string>{constcdb=newUint8Array([0x12,0x00,0x00,0x00,0x24,0x00]);constresponse=awaitsendSCSICommand(device,endpoints,cdb,36,'in');if(response.status!==0){thrownewError('INQUIRY命令失败');}// 提取厂商信息(8字节)和产品信息(16字节)constvendor=String.fromCharCode(...response.data.slice(8,16)).trim();constproduct=String.fromCharCode(...response.data.slice(16,32)).trim();return`${vendor}${product}`;}asyncfunctionscsiReadCapacity(device:usbManager.USBDevice,endpoints:EndpointInfo):Promise<number>{constcdb=newUint8Array([0x25,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00]);constresponse=awaitsendSCSICommand(device,endpoints,cdb,8,'in');if(response.status!==0){thrownewError('READ CAPACITY失败');}// 返回总扇区数(大端)constview=newDataView(response.data.buffer);constblocks=view.getUint32(0,false);constblockSize=view.getUint32(4,false);returnblocks*blockSize;}

第九步:读取根目录扇区并解析文件名

这是一个“足够简单”的解析:假定U盘是FAT32格式。我们读取第一个扇区(LBA 0)判断是否为MBR,然后找到第一个分区,再读取分区的引导扇区获取根目录信息。实际代码会很长,这里给出关键函数:

asyncfunctionreadSector(device:usbManager.USBDevice,endpoints:EndpointInfo,lba:number,sectorSize:number):Promise<Uint8Array>{constcdb=newUint8Array(10);cdb[0]=0x28;// READ10cdb[2]=(lba>>24)&0xFF;cdb[3]=(lba>>16)&0xFF;cdb[4]=(lba>>8)&0xFF;cdb[5]=lba&0xFF;cdb[7]=(sectorSize>>8)&0xFF;cdb[8]=sectorSize&0xFF;constresponse=awaitsendSCSICommand(device,endpoints,cdb,sectorSize*512,'in');if(response.status!==0){thrownewError('READ10失败');}returnresponse.data;}functionparseFAT32RootDirectory(sectorData:Uint8Array):string[]{constfiles:string[]=[];// 每个目录项32字节for(leti=0;i<sectorData.length;i+=32){if(sectorData[i]===0x00)break;// 空条目结束if(sectorData[i]===0xE5)continue;// 已删除constattr=sectorData[i+11];if(attr&0x08)continue;// 卷标// 提取文件名(8.3格式)letname='';for(letj=0;j<8;j++){constc=sectorData[i+j];if(c===0x20)break;name+=String.fromCharCode(c);}constext=[];for(letj=8;j<11;j++){constc=sectorData[i+j];if(c===0x20)break;ext.push(String.fromCharCode(c));}if(ext.length>0)name+='.'+ext.join('');files.push(name);}returnfiles;}

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

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

立即咨询