USB通信

​ 通过获取UsbManager来检索所需的UsbDevice。有了设备后,您需要查找相应的UsbInterface及其UsbEndpoint进行通信后会正确的端点后,打开UsbDeviceConnection与USB设备通信。

显示主机模式和配件模式之间的差异的示意图

  • UsbManager : 可以枚举USB设备并与之通信
  • UsbDevice: 连接的USB设备,并包含用于访问其标识的方法信息,接口和端点。
  • UsbInterface: 表示USB设备的接口,用于定义设备。设备可以是一个或多个用于通信的接口
  • UsbEndpoint : 表示接口端点,是此接口的通信通道,一个接口可以有一个或多个端点,并且通常用于实现与设备的双向通信。
  • UsbdeviceConnection: 表示与设备的连接,可在端点上传数据,允许同步或异步来回发送数据
  • UsbRequest: 通过UsbDeviceConnection与设备通信的异步请求
  • UsbConsants: 定义与 Linux 的 linux/usb/ch9.h 中的定义对应的 USB 常量 内核版本。

Usb 接口示例

USB权限与特征声明

<uses-permission android:name="android.permission.USB_PERMISSION" />
<uses-feature android:name="android.hardware.usb.host" />

初始化USB通信

//设备管理器
private UsbManager usbManager;
//连接的设备
private UsbDevice currentDevice = null;
//连接状态类
private UsbDeviceConnection connection;
//USB连接接口
private UsbInterface usbInterface;

//请求权限
private String ACTION_USB_PERMISSION = "com.your.package.USB_PERMISSION";
private BroadcastReceiver mUsbBroadcast;
 
/**
 * USB请求权限
 */
private class UsbBroadcast extends BroadcastReceiver {
    public void onReceive(Context paramAnonymousContext, Intent paramAnonymousIntent) {
        try {

            String action = paramAnonymousIntent.getAction();
            // 判断广播类型
            if (ACTION_USB_PERMISSION.equals(action)) {
                try {
                    // 如果用户同意,则对读写器进行操作
                    connectDevice();
                } catch (Exception e) {
                    disconnectDevice();
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

注册USB广播接收器

IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
registerReceiver(usbReceiver, filter);

搜索USB设备

/**
 * 获取Usb列表
 * @return
 */
public List<UsbDevice> getUsbPrinters() {
    List<UsbDevice> usbDevices = new ArrayList<>();
    usbManager = (UsbManager) App.getContext().getSystemService(Context.USB_SERVICE);
    HashMap<String, UsbDevice> deviceList = usbManager.getDeviceList();
    Iterator<UsbDevice> deviceIterator = deviceList.values().iterator();

    while (deviceIterator.hasNext()) {
        UsbDevice device = deviceIterator.next();
        if (isCscidDevice(device)) {
            usbDevices.add(device);
        }
    }
    return usbDevices;
}


/**
 * 判断UsbDevice 是否为读卡器类型设备
 * @param device
 * @return true:读卡器设备
 */
public boolean isCscidDevice(UsbDevice device) {
    if (device == null) {
        return false;
    }
    if (device.getInterfaceCount() == 0) {
        return false;
    }
    for (int i = 0; i < device.getInterfaceCount(); i++) {
        android.hardware.usb.UsbInterface usbInterface = device.getInterface(i);

        if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_HID) {
            int productId = device.getProductId();
            int vendorId = device.getVendorId();
            // 此处根据自己设备的厂商ID以及设备ID来进行修改
            if (productId == 10203 && vendorId == 5824) {
                return true;
            }
        }
    }
    return false;
}

连接设备

/**
 * 连接设备
 */
public void connectDevice() {
    try {
//权限请求
/*
* 在使用USB读写器设备前,应用必须获得权限。
* 为了确切地获得权限,首先需要创建一个广播接收器。在调用requestPermission()这个方法时从得到的广播中监听这个意图。
* 通过调用requestPermission()这个方法为用户跳出一个是否连接该设备的对话框。
*/
        if (mUsbBroadcast == null) {
            mUsbBroadcast = new UsbBroadcast();
            IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
            App.getContext().registerReceiver(mUsbBroadcast, filter);
        }

        //获取全部设备
        if (currentDevice == null) {
            List<UsbDevice> usbDeviceList = getUsbPrinters();
            if (usbDeviceList.size() > 0) {
                currentDevice = usbDeviceList.get(0);
            }
        }

        if (currentDevice != null) {
            // 检查设备是否具有所需的权限
            if (usbManager.hasPermission(currentDevice)) {
                // 打开设备并进行通信
                if (connection == null) {
                    connection = usbManager.openDevice(currentDevice);
                }
                if (connection != null) {
                    // 获取接口并配置
                    if (usbInterface == null) {
                        usbInterface = currentDevice.getInterface(0);
                    }
                    connection.claimInterface(usbInterface, true);

                    // 你可以在这里与设备进行通信
                    //readDataFromDevice();
                    Log.d(TAG, "连接到设备: " + currentDevice.getDeviceName());
                } else {
                    Log.e(TAG, "无法打开设备: " + currentDevice.getDeviceName());
                }
            } else {
                // 如果没有则请求权限
                PendingIntent mPermissionIntent = PendingIntent.getBroadcast(JuAiApp.getContext(), 0,
                        new Intent(ACTION_USB_PERMISSION), PendingIntent.FLAG_IMMUTABLE);
                usbManager.requestPermission(currentDevice, mPermissionIntent);
            }
        }
    } catch (Exception r) {
        r.printStackTrace();
    }

}

发送数据

UsbEndpoint.getType 获取USB 端点的传输类型

  • UsbConstants.USB_ENDPOINT_XFER_CONTROL : 控制端点,忽悠啊用于设备的枚举(Enumeration),配置和状态查询
  • UsbConstants.USB_ENDPOINT_XFER_BULK : 批量端点 ,主要用于传输大量对时间不敏感的数据,它依靠硬件重传机制来保证数据的完整性(100%可靠),但无法保证传输的带宽和延迟
  • UsbConstants.USB_ENDPOINT_XFER_INT 中断端点,用于传输数量小,但对相应延迟有要求的数据,Host会周期性的轮询(Polling)该端点以获取数据,典型的输入设备HID,如USB鼠标,键盘,触摸屏,以及电源状态或特定的系统硬件中断事件上报。
  • UsbConstants.USB_ENDPOINT_XFER_ISOC : 同步/等时端点,用于连续的,实时性极高的数据传输,它能保证稳定的带宽和发送速率,但没有重传机制(允许丢包) ,不保证数据的绝对完整性,UVC(USB摄像头) 视频流输出,UAC(USB声卡)音频数据等。

bulkTransfer 传输数据.

  • endpoint(UsbEndpoint) : 与USB通信的事务端点
  • buffer(bute[]) : 用于发送或接受数据的缓冲区
  • offset(int): 缓冲区中要发送或接受的第一个字节的索引(偏移量)
  • length(int): 要发送或接受的数据长度。
    • Build.VERSION_CODES.P Android 9 之前,大于16384 字节的值 会被截断为 16384.
    • Build.VERSION_CODES.P 更高版本,任何长度值都是有效的。
  • timeout(int): 超时时间(以毫秒为单位),0表示无限期等待。
public int bulkTransfer (UsbEndpoint endpoint, 
                byte[] buffer, 
                int offset, 
                int length, 
                int timeout)
  • 默认从头开始:该方法默认从buffer字节数组的索引0位置开始发送或接受数据
bulkTransfer(endpoint, buffer, length, timeout)
  • 添加了offset参数,允许你精确指定从buffer数组的哪个索引位置开始传输数据。
bulkTransfer(endpoint, buffer, offset, length, timeout)
  • 读写 判断 根据endpoint.getDirection() 来判断数据的流向, 发送与接受 站在 Android设备端来思考。
    • USB_DIR_OUT: 发送/写,发送是从Host (Android 设备系统端) 流向Device (外部挂载的USB外设)。如果传入的端点是OUT端点,bulkTransfer就会将你提供的buffer里的数据发送给外设。
    • USB_DIR_IN (接受/读):方向是从Device(外设)流向Host,如果传入的端点是IN端点,bulkTransfer 会进入监听状态,将外设发来的数据接受并填充到buffer中
/**
 * 发送数据
 *
 * @param message
 */
UsbEndpoint endpointOut = null; // 接口端点

private void sendDataToDevice(String message) {
    // 获取输出端点
    if (endpointOut == null) {
        for (int i = 0; i < usbInterface.getEndpointCount(); i++) {
            UsbEndpoint endpoint = usbInterface.getEndpoint(i);
            // 获取 USB传输 类型,同时输出方向 输出OUT
            if (endpoint.getType() == UsbConstants.USB_ENDPOINT_XFER_BULK &&
                    endpoint.getDirection() == UsbConstants.USB_DIR_OUT) {
                endpointOut = endpoint; // 获取输出端点
                break;
            }
        }

    }

    if (endpointOut == null || connection == null) {
        Log.e(TAG, "输出端点或连接未初始化");
        return;
    }

    byte[] buffer = message.getBytes(); // 将要发送的字符串转换为字节数组
    //connection 是 连接设备之后 返回的 UsbDeviceConnection
    // 在给定端点上执行一个传输
    int bytesSent = connection.bulkTransfer(endpointOut, buffer, buffer.length, 1000); // 1秒超时
    if (bytesSent < 0) {
        Log.e(TAG, "发送数据失败");
    } else {
        Log.d(TAG, "成功发送数据: " + message);
    }
}

读取数据

/**
 * 读取数据
 */
UsbEndpoint endpointIn = null;
private static final int BUFFER_SIZE = 1024; // 定义读取的缓冲区大小

private void readDataFromDevice() {
    if (endpointIn == null) {
        // 获取输入端点
        for (int i = 0; i < usbInterface.getEndpointCount(); i++) {
            UsbEndpoint endpoint = usbInterface.getEndpoint(i);
            Log.e(TAG, "readDataFromDevice: " + endpoint.getType());
            Log.e(TAG, "getDirection: " + endpoint.getDirection());
            // 获取中断数据端点
            if (endpoint.getType() == UsbConstants.USB_ENDPOINT_XFER_INT &&
                    endpoint.getDirection() == UsbConstants.USB_DIR_IN) {
                endpointIn = endpoint; // 获取输入端点
                break;
            }
        }
    }

    if (endpointIn == null || connection == null) {
        Log.e(TAG, "输入端点或连接未初始化");
        return;
    }


    byte[] buffer = new byte[BUFFER_SIZE]; // 创建缓冲区

    // 循环读取数据
    Thread readThread = new Thread(new Runnable() {
        @Override
        public void run() {
            while (true) {
                int bytesRead = connection.bulkTransfer(endpointIn, buffer, buffer.length, 1000); // 1秒超时
                if (bytesRead > 0) {
                    // 处理读取到的数据
                    String data = new String(buffer, 0, bytesRead); // 将字节数组转换为字符串
                    Log.d(TAG, "读取数据: " + data);
                } else if (bytesRead == 0) {
                    Log.d(TAG, "没有更多数据可读");
                    break;
                } else {
                    Log.e(TAG, "读取数据失败: " + bytesRead);
                    break;
                }
            }
        }
    });
    readThread.start(); // 启动读取线程
}

断开设备并关闭资源

/**
 * 断开设备
 */
public void disconnectDevice() {
    try {
        if (usbManager != null && currentDevice != null && usbInterface != null) {
            // 获取之前连接的接口
            // 释放接口
            connection.releaseInterface(usbInterface);
            // 关闭连接
            connection.close();
            // 清空连接
            connection = null;
            usbInterface = null;
            //读取置空
            endpointIn = null;
            //写入置空
            endpointOut = null;
            //设备置空
            currentDevice = null;
            if (mUsbBroadcast != null) {
                App.getContext().unregisterReceiver(mUsbBroadcast);
            }
            Log.d(TAG, "设备已断开: ");
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

USB协议相关

USB常见类型

USB设备类型

//获取设备类型
for (int i = 0; i < device.getInterfaceCount(); i++) {
    android.hardware.usb.UsbInterface usbInterface = device.getInterface(i);
    /**
     * 设备类 ID	设备类名称	描述
     * 0x01	Audio	音频设备,如 USB 音频接口。
     * 0x02	Communications	通信设备,如调制解调器和网络适配器。
     * 0x03	HID (Human Interface Device)	人机接口设备,通常指键盘、鼠标等。
     * 0x05	Physical Interface Device	物理接口设备,通常用于各种控制设备。
     * 0x06	Image	图像设备,如 USB 摄像头和扫描仪。
     * 0x07	Printer	打印机设备。
     * 0x08	Mass Storage	大容量存储设备,如 USB 闪存驱动器和外部硬盘。
     * 0x09	Hub	USB 集线器。
     * 0x0A	CDC (Communications Device Class)	通信设备类,常用于调制解调器和串行口。
     * 0x0B	Smart ICCard	智能卡读卡器。
     * 0x0D	Content Security Device	内容保护设备,用于安全性应用。
     * 0x0E	Video	视频设备,如视频采集卡。
     * 0xEF	Miscellaneous	其他不属于上述类别的设备。
     * 0xFF	Vendor Specific	厂商自定义设备,通常特定于某个硬件厂商的设备。
     * @param deviceClass
     * @param deviceSubclass
     * @param deviceProtocol
     * @return
     */
    if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_HID) {
        int productId = device.getProductId();
        int vendorId = device.getVendorId();
        if (productId == 10203 && vendorId == 5824) {
            return true;
        }
    }
}

设备信息参数

//获取设备类型
if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_HID) {
   //产品ID
	int productId = device.getProductId();
	//厂商ID
	int vendorId = device.getVendorId();
	if (productId == 10203 && vendorId == 5824) {
		return true;
	}
}
* DeviceName=/dev/bus/usb/005/007         USB端口名字
* ProductName=null                        产品名字
* DeviceId=5007(0x138F)                   设备ID
* VendorId=5824(0x16C0)                   厂商ID
* ProductId=10203(0x27DB)                 产品ID
* DeviceClass=0                           设备分类
* device Class 为0-------------      
* Interface.describeContents()=0          接口描述
* Interface.getEndpointCount()=2          接口端点数量
* Interface.getId()=0                     ID接口
* Interface.getInterfaceClass()=3         接口数量
* anInterface.getInterfaceProtocol()=0    接口协议数量
* anInterface.getInterfaceSubclass()=0
* device Class 为0------end-------

注意事项

  • 扫码枪USB插拔时,会照成Activity重新加载,说明对Activity生命周期有影响,可以再Activity配置
android:configChanges="orientation|keyboard|keyboardHidden"

扫码枪设备

  • USB键盘模式,自感模式(出厂默认): 大多数扫码枪是模拟键盘进行输入(不支持 包含有汉字的二维码),大多数时候需要使用带焦点的EditText 接受其扫描的信息,设置一个带焦点的EditText进行接受,此方式容易漏读扩展的ASCII码数据,除非二维码是标准的ASCII码(1-128)。由于扫码枪会将扫描出来的内容转化为键盘事件,对应的就是Android中的KeyEvent事件,所以我们只需要在我们的activity中重写dispatchKeyEvent方法,即可获取相应事件。
  • 通过USB COM连接扫码:发送扫码开关指令进行扫码,即基于USB相关类控制扫码开关
  • USB串口自感模式:即串口连接(当扫码枪使用USB通讯接口,但主机应用程序是采用串口通讯方式接受数据),客户端只负责监听读取数据,无需控制扫码枪指令,需要依赖usb串口传输数据的第三方库。https://github.com/mik3y/usb-serial-for-android