两个Android设备间建立配件模式
Android配件模式简介:
如下图所示,Android设备比如手机作为USB device方,配件可以是电脑也可以是android设备作为USB host方(必须能提供500ma的5v供电)。注意配件和配件模式的区别,配件是作为host来连接android设备,配件模式是指android device设备被host设置成了配件模式。
建立配件模式的过程:
-
配件通过USB驱动的控制通道(0通道,端点0)获取手机的VID和PID。
-
配件分析获取的vendor id是否为0x18d1,以及product id是否为0x2D01,0x2D02,0x2D03,0x2D04,0x2D05。如果均符合,则说明手机现在已经是配件模式,可以按照配件模式的要求直接重新配置USB端点和接口。否则启动尝试进入配件模式流程。
-
确定手机已经进入配件模式,重新枚举USB设备,手机重新进行USB协商。
-
按照配件模式重新配置USB端点和接口,建立配件模式的数据通道。
尝试启动配件交互模式流程:
-
配件发送序号51的USB请求报文,手机收到后查询自己的AOA协议版本,发送响应报文给配件。
-
配件校验协议版本号,目前为1或2,其他的均不支持。
-
配件发送序号52的USB请求报文,通过Index字段携带配件自身信息,包括制造商、型号及版本、设备描述、序列号、URI等,手机根据这些信息启动响应的APP。
-
配件发送序号53的USB请求报文,切换USB模式,主要是根据切换VID/PID。
-
重新枚举USB设备,准备建立AOA数据通道。
AOA设备的握手过程:
绝大多数Android设备,在缺省情况下都不挂载Accessory驱动,在Accessory和Android设备建立USB连接时;Accessory会通过握手协议查询该设备是否为android设备且具有AOA支持,如果获得正确应答,Accessory会向Adnroid设备发出切换到AOA模式的请求,Android设备会执行该请求,将USB切换成AOA模式;在这个过程中,USB连接会出现一次逻辑插拔,USB host一端会重新枚举设备。
在握手过程中,Accessory会向Android提供AOA约定的描述信息,其中有3个信息是Android系统用于绑定Accessory设备与APP的;分别是:manufacturer、model、version。Android系统根据这3个字符串匹配相应的APP。
如果系统内无任何APP可以匹配Accessory发来的握手消息;则Adnroid设备会弹出一个对话框,向用户提供Accessory设备发送来的描述信息和URL信息,用户可以点击URL所指向的web页面。
如果系统有app可以匹配accessory发来的握手消息;则android会弹出一个对话框询问用户是否立刻启动该app;如果用户选择ok则启动该app;同时该对话框提供一个勾选项,勾选之后每次accessory设备连接后会自动启动该app;应该要求用户勾选该对话框,否则app启动后向UsbManager获取accessory设备后可能因为permission问题无法打开文件描述符建立通信连接。
注意:
-
pid为0x2D00是保留给支持附件模式的Android设备。0x2D01保留用于支持附件模式以及android调试桥adb协议的设备,第二个接口为adb并且具有两个批量端点。如果要在计算机上模式附件,可以使用这些端点来调试附件程序。一般来说,不要使用此接口,除非配件在设备上实现传输到adb。
0x2D00 具有一个拥有2个批量端点的接口,用于输出和输入通信的。
0x2D01 具有两个接口,每个接口有2个批量端点,用于输入和输出通信。第一个接口处理标准通信,第二个接口处理adb通信。要使用接口,请找到第一个批量输入和输出端点,会用SET_CONFIGURATION(0x09)设备请求将设备配置设置为1,然后使用端点进行通信。
-
AOA目前不支持同时进行AOA和MTP(媒体传输协议)连接。要从AOA切换到MTP,配件必须首先断开USB设备(物理上或电气上等效方式),然后使用MTP重新连接。
实现:
关于AOA的host端在android上实现的代码在网上有很多,这里不再贴出来。
关于AOA的accessory配件端在android上怎么实现呢,网上一般找出来的都是linux下模式的adk方式,其实用android做配件原理是一样的,就是得转换成java代码方式来实现。
涉及到的几个常量定义:
/* 配件模式AOA支持: VID 固定为Google的官方VID – 0x18D1 PID 在不同的模式下定义如下: ● 0x2D00 - V1 accessory ● 0x2D01 - V1 accessory + adb ● 0x2D02 - V2 audio, 注意V2兼容V1,上面两个仍然支持。,注意:AOAv2音频支持在Android 8.0中已被弃用 ● 0x2D03 - V2 audio + adb,注意:AOAv2音频支持在Android 8.0中已被弃用 ● 0x2D04 - V2 accessory + audio,注意:AOAv2音频支持在Android 8.0中已被弃用 ● 0x2D05 - V2 accessory + audio + adb,注意:AOAv2音频支持在Android 8.0中已被弃用 */
/** * USB请求类型requestType按位定义: * define USB_SETUP_HOST_TO_DEVICE 0x00 // Device Request bmRequestType transfer direction - host to device transfer * define USB_SETUP_DEVICE_TO_HOST 0x80 // Device Request bmRequestType transfer direction - device to host transfer * define USB_SETUP_TYPE_STANDARD 0x00 // Device Request bmRequestType type - standard * define USB_SETUP_TYPE_CLASS 0x20 // Device Request bmRequestType type - class * define USB_SETUP_TYPE_VENDOR 0x40 // Device Request bmRequestType type - vendor * define USB_SETUP_RECIPIENT_DEVICE 0x00 // Device Request bmRequestType recipient - device * define USB_SETUP_RECIPIENT_INTERFACE 0x01 // Device Request bmRequestType recipient - interface * define USB_SETUP_RECIPIENT_ENDPOINT 0x02 // Device Request bmRequestType recipient - endpoint * define USB_SETUP_RECIPIENT_OTHER 0x03 // Device Request bmRequestType recipient - other * USB配件模式下PID/VID定义: * #define USB_ACCESSORY_VENDOR_ID 0x18D1 * #define USB_ACCESSORY_PRODUCT_ID 0x2D00 * #define USB_ACCESSORY_ADB_PRODUCT_ID 0x2D01 * USB请求序号: * #define ACCESSORY_STRING_MANUFACTURER 0 * #define ACCESSORY_STRING_MODEL 1 * #define ACCESSORY_STRING_DESCRIPTION 2 * #define ACCESSORY_STRING_VERSION 3 * #define ACCESSORY_STRING_URI 4 * #define ACCESSORY_STRING_SERIAL 5 * #define ACCESSORY_GET_PROTOCOL 51 * #define ACCESSORY_SEND_STRING 52 * #define ACCESSORY_START 53 * 配件发送序号51的USB请求报文,得到协议信息,即对方(手机)是否支持aoa模式 * 配件发送序号52的USB请求报文,通过Index字段携带配件自身信息,包括制造商、型号、版本、设备描述、序 列号URI等。手机根据这些信息启动响应的APP * 配件发送序号53的USB请求报文,切换USB模式,主要是根据切换的vendorID和productID * 重新枚举USB设备,准备建立AOA数据通道
**/
1. 配件作为host,插入或拔出usb线时都会收到UsbManager.ACTION_USB_DEVICE_DETACHED和UsbManager.ACTION_USB_DEVICE_ATTACHED广播。 2. 用getDeviceList()扫描所有usb设备,找到自己所需的usb配件端口。 3. 查看该配件是否已经处于配件模式,如果是2d00/18d1,则用openDevice(device)直接进入通信:
//最好关闭掉Host端Android设备的USB ADB调试功能(android11以后可以用wifi无线调试替代usb调试), // 否则在0X2D01下2个interface难以区分那个接口是adb,哪个接口是aoa。
mUsbInterface = device.getInterface(0); for(int j=0; j< 2; j++) { if (UsbConstants.USB_DIR_IN == mUsbInterface.getEndpoint(j).getDirection()) { mUsbEndpointIn = mUsbInterface.getEndpoint(j); mUsbInMax = mUsbEndpointIn.getMaxPacketSize(); } if (UsbConstants.USB_DIR_OUT == mUsbInterface.getEndpoint(j).getDirection()) { mUsbEndpointOut = mUsbInterface.getEndpoint(j); mUsbOutMax = mUsbEndpointOut.getMaxPacketSize(); } } // requestType: USB_SETUP_HOST_TO_DEVICE | USB_SETUP_TYPE_STANDARD | USB_SETUP_RECIPIENT_DEVICE // request: ACCESSORY_SET_CONFIGURATION int rtn = connection.controlTransfer(0x00, 9, 1, 0, null, 0, IAoaConst.USB_TIMEOUT_IN_MS); if(rtn < 0){ tmpStr = "enterCommunication: controlTransfer return error:" + rtn; Log.i(TAG,tmpStr); SendMsgToMainActivity(tmpStr); return false; } //发送一次 byte[] buffer = new byte[]{'a', 'b', 'c', 'd'}; connection.bulkTransfer(mUsbEndpointOut, buffer, buffer.length, IAoaConst.USB_TIMEOUT_IN_MS); tmpStr = "enterCommunication: connection.bulkTransfer to peer."; Log.i(TAG,tmpStr); SendMsgToMainActivity(tmpStr); //接收消息的线程 new CommunicationThread().start();
private class CommunicationThread extends Thread { @Override public void run() { Log.w(TAG, "CommunicationThread: start receive."); running = true; byte[] buffer = new byte[1024]; while (running) { //Handle incoming messages if(mUsbEndpointIn!=null) { int len = connection.bulkTransfer(mUsbEndpointIn, buffer, buffer.length, IAoaConst.USB_TIMEOUT_IN_MS); if (len >= 0) { Log.w(TAG, "CommunicationThread: received msg, len: " + len); //Log.w(TAG, "CommunicationThread: received msg, content: " + new String(buffer)); }else{ Log.w(TAG, "CommunicationThread: received error!"); } } } } }
4. 如果不是,则需要进入初始化过程,还是openDevice(device)打开设备,然后用51号请求得到协议号:
private int getProtocol(UsbDeviceConnection connection) { byte[] buffer = new byte[]{0,0}; // requestType: USB_SETUP_DEVICE_TO_HOST | USB_SETUP_TYPE_VENDOR | USB_SETUP_RECIPIENT_DEVICE // request: ACCESSORY_GET_PROTOCOL int rtn = connection.controlTransfer(0xC0, 51, 0, 0, buffer, 2, IAoaConst.USB_TIMEOUT_IN_MS); if(rtn <=0){ String tmpStr = "getProtocol: controlTransfer return error:" + rtn; Log.i(TAG,tmpStr); SendMsgToMainActivity(tmpStr); } String tmpStr = "getProtocol: protocol:" + buffer[0]+"."+buffer[1]; Log.i(TAG,tmpStr); SendMsgToMainActivity(tmpStr); return rtn; } 5. 然后通过52号请求设置参数:
//支持aoa协议,则控制对端(手机)启动特定的application。 boolean res = initStringControlTransfer(connection, 0, IAoaConst.USB_AOA_MANUFACTURER); // MANUFACTURER res = res&&initStringControlTransfer(connection, 1, IAoaConst.USB_AOA_MODEL); // MODEL res = res&&initStringControlTransfer(connection, 2, IAoaConst.USB_AOA_DESCRIPTION); // DESCRIPTION res = res&&initStringControlTransfer(connection, 3, IAoaConst.USB_AOA_VERSION); // VERSION res = res&&initStringControlTransfer(connection, 4, IAoaConst.USB_AOA_URI); // URI res = res&&initStringControlTransfer(connection, 5, IAoaConst.USB_AOA_SERIAL); // SERIAL
private boolean initStringControlTransfer(final UsbDeviceConnection deviceConnection, final int index, final String string) { // requestType: USB_SETUP_HOST_TO_DEVICE | USB_SETUP_TYPE_VENDOR | USB_SETUP_RECIPIENT_DEVICE // request: ACCESSORY_SEND_STRING int rtn = deviceConnection.controlTransfer(0x40, 52, 0, index, string.getBytes(), string.length(), IAoaConst.USB_TIMEOUT_IN_MS); if(rtn <=0){ String tmpStr = "initStringControlTransfer: controlTransfer return error:" + rtn; Log.e(TAG,tmpStr); SendMsgToMainActivity(tmpStr); return false; } return true; }
6. 最后使用53号请求启动AOA模式:
// requestType: USB_SETUP_HOST_TO_DEVICE | USB_SETUP_TYPE_VENDOR | USB_SETUP_RECIPIENT_DEVICE // request: ACCESSORY_START connection.controlTransfer(0x40, 53, 0, 0, new byte[]{}, 0, IAoaConst.USB_TIMEOUT_IN_MS);
7. 此时USB会做一次逻辑插拔,等attach到来后,会发现之前的usb pid/vid已经成了2d00/18d1,此时即可进入通信了。