最近项目中有需要连接 USB 设备,App 运行在平板之上,只有一个 type-c 接口,只能通过 type-c 转串口进行连接,所以 App 直接接触的是 USB 设备。
使用 USB 设备遵照Google 官方文档开发即可。
配置清单
1 | <manifest ...> |
device_filter 可过滤具有指定属性的所有 USB 设备:1
2
3
4"1.0" encoding="utf-8" xml version=
<resources>
<usb-device vendor-id="1234" product-id="5678" class="255" subclass="66" protocol="1" />
</resources>
使用设备
- 发现连接的 USB 设备,具体方法是使用 Intent 过滤器在用户连接 USB 设备时接收通知,或者枚举已连接的 USB 设备。
- 请求用户授予连接到 USB 设备的权限(如果尚未获得权限)。
- 通过在适当的接口端点读取和写入数据来与 USB 设备通信。
1 | object UsbDeviceManager { |
封装
USB 有多种芯片,针对不同的芯片可能需要做识别,在 Github 上找到一个库UsbSerial,针对 USB 设备做了许多的封装,硬件同事提供的转串口工具为 CH34X 系列,该库也支持。参照文档使用起来很方便。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28public class UsbDeviceCommunicator extends BaseCommunicator {
private static final int BufferSize = 32;
private UsbSerialDevice usbDevice;
public void open() {
UsbDeviceManager.open(MucangConfig.getContext(), new UsbDeviceManager.OnDeviceConnectListener() {
public void onDeviceConnect(@NotNull UsbSerialDevice serial) {
usbDevice = serial;
// Usb 串口读取 byte[] 是读满才返回,把 bufferSize 搞小点
mInputStream = new BufferedInputStream(serial.getInputStream(), BufferSize);
mOutputStream = new BufferedOutputStream(serial.getOutputStream());
startReadThread();
}
});
}
public void close() {
super.close();
if (usbDevice != null) {
usbDevice.syncClose();
}
}
}
抽象
因为串口的不一致性,项目中有 3 中流传输方式:蓝牙、串口、USB。数据读取解析方式完全一致,所以抽象出 Communicator:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42public interface Communicator {
/**
* 打开串口
*/
void open() throws Exception;
/**
* 发送数据
*/
@WorkerThread
void send(SendData sendData);
/**
* 处理接收的数据
*
* @param readBytes 只包含数据域数据
*/
void parseData(ReadBytes readBytes);
/**
* 关闭串口
*/
void close();
/**
* 释放资源
*/
void release();
/**
* 大小端
*
* @return
*/
boolean littleEndian();
/**
* 串口是否可用
*/
boolean isAvailable();
}
BaseCommunicator:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238public abstract class BaseCommunicator implements Communicator {
private static final String TAG = "BaseCommunicator";
private static final long SEND_INTERVAL = 10; // 避免多线程发送粘包,添加延时
protected BufferedInputStream mInputStream; // 输入流,子类进行初始化
protected BufferedOutputStream mOutputStream; // 输出流,子类进行初始化
private DefaultReadThread mReadThread; // 读线程
private final SignalHandler signalHandler; // 消息分发 handler
private long lastSendTime; // 上一次发包的时间
public BaseCommunicator() {
this.signalHandler = new SignalHandler();
}
public synchronized void send(SendData sendData) {
if (!isAvailable()) {
return;
}
if (interceptMessage(sendData.toBytes())) {
return;
}
try {
byte[] finalData = SerialProtocol.createProtocol(sendData.toBytes());
if (mOutputStream != null) {
long curTime = SystemClock.elapsedRealtime();
if (curTime - lastSendTime >= SEND_INTERVAL) {
lastSendTime = curTime;
} else {
long delta = SEND_INTERVAL - (curTime - lastSendTime);
SystemClock.sleep(delta);
lastSendTime = SystemClock.elapsedRealtime();
}
mOutputStream.write(finalData, 0, finalData.length);
mOutputStream.flush();
}
} catch (Throwable e) {
LogUtils.e(TAG, "sendData() error", e);
}
}
public void parseData(ReadBytes readBytes) {
readBytes.reset();
int msgType = readBytes.readUnsignedByte();
signalHandler.dispatchHandler(msgType, readBytes);
}
/**
* 当ota正在升级的时候不下发其他业务消息,ota升级优先级最高
*/
private boolean interceptMessage(byte[] bytes) {
// 升级过程中
if (CommunicateManager.getInstance().isNeedInterceptor()) {
// 解析指令类型
int order = bytes[0] & 0xff;
// 只放行 ota 指令
boolean isOtaOrder = SingleFirmwareUpgradeProcessor.isOtaOrder(order);
return !isOtaOrder;
}
return false;
}
public synchronized void close() {
if (mReadThread != null && mReadThread.isAlive()) {
mReadThread.close();
mReadThread = null;
}
IOUtils.close(mInputStream);
IOUtils.close(mOutputStream);
lastSendTime = 0;
}
public synchronized void release() {
close();
}
public boolean isAvailable() {
if (mReadThread == null || !mReadThread.isAlive()) {
return false;
}
return true;
}
public boolean littleEndian() {
return true;
}
/**
* 启动串口读线程
*/
protected void startReadThread() {
mReadThread = new DefaultReadThread();
mReadThread.start();
LiveBus.post(new CommunicatorOpenSuccess());
}
/**
* 读满一个数组
*/
private static void readFully(InputStream in, byte[] array, int off, int len) throws IOException {
if (len < 0)
throw new IndexOutOfBoundsException();
int n = 0;
while (n < len) {
int count = in.read(array, off + n, len - n);
if (count < 0)
throw new EOFException();
n += count;
}
}
private static void readFully(InputStream in, byte[] array) throws IOException {
readFully(in, array, 0, array.length);
}
private static long crc(byte[] data) {
CRC32 crc = new CRC32();
crc.update(data);
return crc.getValue();
}
/**
* 默认读线程
*/
private class DefaultReadThread extends Thread {
private volatile boolean mRunning = true;
DefaultReadThread() {
}
/**
* 读取一帧数据
*/
private byte[] readFrame() throws IOException {
//header识别,一直识别到一帧为止,否则一直等待
byte h1, h2 = 0;
while (mRunning) {
h1 = (byte) mInputStream.read();
if (h2 == SerialProtocol.HEAD1 && h1 == SerialProtocol.HEAD2) {
break;
}
h2 = (byte) mInputStream.read();
if (h1 == SerialProtocol.HEAD1 && h2 == SerialProtocol.HEAD2) {
break;
}
}
//帧长
int length = mInputStream.read();
//数据
byte[] data = new byte[length - 5];
readFully(mInputStream, data);
//校验数据
byte[] crcArray = new byte[4];
readFully(mInputStream, crcArray);
ReadBytes readCrc = new ReadBytes(crcArray, littleEndian());
int crc = readCrc.readInt();
//结束数据
byte[] eof = new byte[2];
readFully(mInputStream, eof);
//crc校验
if (!(true || crc(data) == crc)) {
return null;
}
//结束符校验,其实这个要不要都无所谓,双重校验了
if (!(eof[0] == SerialProtocol.END && eof[1] == SerialProtocol.END)) {
return null;
}
return data;
}
public void run() {
while (mRunning) {
try {
if (mInputStream == null) {
// 暂停一点时间,免得一直循环造成CPU占用率过高
Thread.sleep(10);
continue;
}
byte[] data = readFrame();
if (data != null) {
parseData(new ReadBytes(data, littleEndian()));
}
} catch (Exception e) {
LogUtils.e(TAG, "DefaultReadThread exception: ", e);
if (e instanceof IOException) {
mRunning = false;
reconnect();
LiveBus.post(new CommunicatorError());
break;
}
}
}
}
/**
* 重连
*/
private void reconnect() {
CommunicateManager.getInstance().release();
MainThreadUtils.postDelayed(new Runnable() {
public void run() {
CommunicateManager.getInstance().start();
}
}, 1500);
}
/**
* 关闭读线程
*/
void close() {
mRunning = false;
try {
this.interrupt();
} catch (Exception e) {
LogUtils.e(TAG, "DefaultReadThread close: ", e);
}
}
}
}
另外 2 个子类:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69public class SerialBoxCommunicator extends BaseCommunicator {
private int mBaudRate; // 串口波特率
private String mDevicePath; // 串口地址
private SerialPort mSerialPort; // 串口
public SerialBoxCommunicator(String devicePath, int baudRate) {
this.mDevicePath = devicePath;
this.mBaudRate = baudRate;
}
public void open() throws OpenSerialPortException {
if (mSerialPort != null) {
close();
}
try {
if (TextUtils.isEmpty(mDevicePath) || mBaudRate == 0) {
throw new RuntimeException("SerialPort hasn't been configured! (device="
+ mDevicePath + ",baudRate=" + mBaudRate);
}
mSerialPort = SerialPort.newBuilder(mDevicePath, mBaudRate).build();
SystemClock.sleep(100);
mInputStream = new BufferedInputStream(mSerialPort.getInputStream());
mOutputStream = new BufferedOutputStream(mSerialPort.getOutputStream());
startReadThread();
} catch (Exception e) {
// 清理数据
close();
// 抛出异常
throw new OpenSerialPortException(e);
}
}
public boolean isAvailable() {
if (mSerialPort == null) {
return false;
}
return super.isAvailable();
}
}
class BluetoothCommunicator extends BaseCommunicator implements BTDataReceiver.InputStreamChangedListener {
private final BTDataReceiver receiver = new BTDataReceiver(MucangConfig.getContext(), this);
public void open() {
receiver.start();
startReadThread();
}
public void send(SendData sendData) {
// 蓝牙模拟,只接收数据,不发送
}
public void close() {
super.close();
receiver.destroy();
}
public void onInputStreamChanged(InputStream inputStream) {
this.mInputStream = new BufferedInputStream(inputStream);
}
}
小坑
UsbDeviceCommunicator 中,获取 mInputStream 时,包装 BufferedInputStream 使用的 bufferSize 设置为 32。三方库封装的 SerialInputStream 读取数据都是通过 usbConnection.bulkTransfer 来实现的:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24/**
* Performs a bulk transaction on the given endpoint.
* The direction of the transfer is determined by the direction of the endpoint.
*
* @param endpoint the endpoint for this transaction
* @param buffer buffer for data to send or receive
* @param offset the index of the first byte in the buffer to send or receive
* @param length the length of the data to send or receive. Before
* {@value Build.VERSION_CODES#P}, a value larger than 16384 bytes
* would be truncated down to 16384. In API {@value Build.VERSION_CODES#P}
* and after, any value of length is valid.
* @param timeout in milliseconds, 0 is infinite
* @return length of data transferred (or zero) for success,
* or negative value for failure
*/
public int bulkTransfer(UsbEndpoint endpoint,
byte[] buffer, int offset, int length, int timeout) {
checkBounds(buffer, offset, length);
if (mContext.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.P
&& length > UsbRequest.MAX_USBFS_BUFFER_SIZE) {
length = UsbRequest.MAX_USBFS_BUFFER_SIZE;
}
return native_bulk_request(endpoint.getAddress(), buffer, offset, length, timeout);
}
length 代表的读取数据的长度,会堵塞,直到读满 buffer。而 FileInputStream 的 read 方法为:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45/*
* @param b the buffer into which the data is read.
* @param off the start offset in array <code>b</code>
* at which the data is written.
* @param len the maximum number of bytes to read.
* @return the total number of bytes read into the buffer, or
* <code>-1</code> if there is no more data because the end of
* the stream has been reached.
* @exception IOException If the first byte cannot be read for any reason
* other than end of file, or if the input stream has been closed, or if
* some other I/O error occurs.
* @exception NullPointerException If <code>b</code> is <code>null</code>.
* @exception IndexOutOfBoundsException If <code>off</code> is negative,
* <code>len</code> is negative, or <code>len</code> is greater than
* <code>b.length - off</code>
* @see java.io.InputStream#read()
*/
public int read(byte b[], int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
} else if (off < 0 || len < 0 || len > b.length - off) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}
int c = read();
if (c == -1) {
return -1;
}
b[off] = (byte)c;
int i = 1;
try {
for (; i < len ; i++) {
c = read();
if (c == -1) {
break;
}
b[off + i] = (byte)c;
}
} catch (IOException ee) {
}
return i;
}
len 代表最大可读取的字节数,不会一直堵塞,直到读满 buffer。 所以需要将 BufferedInputStream 的 bufferSize 设置小一点(默认8192),否则使用默认值一直读满 8192 个字节会等待较久的时间,不符合每 200ms 发送一次 64 byte 的实际场景。
另外,当没有 DevicePermission 时会弹窗,如果一直弹不太合适,所以有一个绕过弹窗验证的思路,可以参考一下,待验证。