安卓设备的USB-HID通讯例程的开发(1)
本博文系JGB联合商务组的原创作品,引用请标明出处
此通讯例程实际名称为【USB-HID调试器】,是在Android Studio 4.1环境下开发的安卓主机与一台HID设备的通讯例子。
按惯例,先上调试场景图,不感兴趣的直接忽略之。
整个外部的HID设备连线非常简洁,成本又十分低廉,但其中的技术含量却不小,很适合于搞毕设的同学拿来参考。
本HID设备(JGB01开发板)使用的芯片仍然是STM32F103C8T6,所烧录的C代码部分将在下一篇博文介绍。
上图是HID设备刚被打开时的情形,它显示: 此HID设备使用了两个非0端点,EP2(IN) 和EP1(OUT),加上0端点共有三个逻辑单元。非0端点的最大数据包长度为28字节。
下图显示了在【取设备状态】的操作完成后所返回的当前设备状态参数,
其中:
- TX-LAST: 表示HID设备最后发送字节数和耗时
- TEMP: HID设备刚上电时的第一次测量温度值
- CUID: HID设备所使用的STM32芯片的唯一且永远不变的标识
- SIZE: Flash的容量
- VERS: 固件开发所用的函数库版本号
依次按动界面中间的: 点亮-熄灭-温控-停止 四个按钮后,安卓设备(作为主机)的屏幕上分别显示了由JGB01开发板回传的数据。
本调试器可运行在手机,机顶盒,安卓电视等设备上,亲测在如下四个版本的安卓设备上运行均稳定可靠,按钮的动作响应也非常的及时流畅:
- 4.2版本的安卓竖屏电视
- 4.4版本的机顶盒
- 8.0版本的手机
- 9.1版本的手机
本调试器所用的软件和硬件结构均继承了我的另一篇博文(以下简称为前文):
因此本文的许多需要说明但又与前文有重复的地方就不再叙述了。同样,本人水平有限错误自然难免,希见谅。但有一点可确认:后面所涉及的JAVA和C源码均在相应的开发工具中是编译通过了的。
USB主机与HID设备的通讯是目前主流的通讯方式之一。
当我开始创建这个Android Studio 4.1的USB通讯项目时我就已默默地把前文多次重点提及的JGB01开发板上的串口桥接器移除了,我也不想再去用什么第三方的开源驱动了,把它弃用了吧。我要开发的是USB-HID设备的通讯,硬件端口用最小板上提供的microUSB插座,软件APP只需引入安卓的USB库就足够了,它们是:
import android.hardware.usb.UsbConstants;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbInterface;
import android.hardware.usb.UsbEndpoint;
import android.hardware.usb.UsbManager;
import android.hardware.usb.UsbRequest;
android.hardware.usb看上去真的是一个含意明确,结构又很分明的包。它清晰地指出了HID设备的三大接收者的层次关系: 设备 - 接口 - 端点
本系统APP界面设计同样继承自前文的【USB串口调试器】,只是增加了一个【清空】按钮,另外改了最前面两个标签:芯片类改为设备类,波特率改为操作符 。
增加的【清空】按钮位于【发送】按钮之后:
<Button
android:layout_marginLeft="4dp"
android:layout_marginTop="2dp"
android:layout_marginBottom="2dp"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="清空"
android:id="@+id/buttClear" />
主活动依然只有一个,很方便你的理解,把本博原创的核心代码贴上相信也是你最为关心的,代码中的许多注释和段落也与前文完全相同,只是增删了一些读写操作的代码块而已,其它的如枚举设备,打开关闭设备,运行期授权,通讯指令定义等则完全一样。
package com.example.jgbusbhid;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.usb.UsbConstants;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbInterface;
import android.hardware.usb.UsbEndpoint;
import android.hardware.usb.UsbManager;
import android.hardware.usb.UsbRequest;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
//需要实现按钮动作接口类: View.OnClickListener 和 AdapterView.OnItemSelectedListener
public class MainActivity extends AppCompatActivity implements View.OnClickListener,
View.OnFocusChangeListener,
AdapterView.OnItemSelectedListener {
private EditText editRead = null;
private EditText editSend = null;
private TextView tvRead = null;
private TextView tvAuthor = null;
private Button buttSend = null;
private Button buttClear = null;
private Button buttOpen = null;
private Button buttClose = null;
private Button buttLedxOn = null;
private Button buttLedxOff = null;
private Button buttTempOn = null;
private Button buttTempOff = null;
private Spinner spChip = null;
private Spinner spBaud = null;
private enum UsbPermission {
Unknown, Requested, Granted, Denied };
private UsbManager usbManager = null;
//针对某一个特定的USB PortA要用到的几个对象组合
private UsbDevice deviceA = null;
private UsbDeviceConnection connectionA = null;
private UsbPermission usbPermissionA = UsbPermission.Unknown;
private static final String INTENT_ACTION_GRANT_USB = "MY_GRANT_USB";
//接收USB设备插入后所发送消息的广播接收器
private BroadcastReceiver broadcastReceiver;
private PendingIntent penIntent = null;
private List<String> chipList = new ArrayList<>();
private List<String> baudList = new ArrayList<>();
private ArrayAdapter<String> adapterChip ;
private ArrayAdapter<String> adapterBaud ;
//标题栏
private ActionBar actionBar = null;
//行缓冲区,每一行最多1024字节,超过将被清空
private byte[] lineBuff ;
//此行的真实长度
private int nLineActLen =0 ;
//[收到数据]editRead这个文本框的当前内容行数
private int nContentRows = 0;
//[收到数据]editRead这个文本框能显示的行数
private int nPageRows =1 ;
//[收到数据]editRead这个文本框的页面计数器
private int nPage =1 ;
private UsbInterface interfaceA =null;
private UsbEndpoint epOut =null;
private UsbEndpoint epIn=null;
//最大包长度 : 28字节 (设为 32 OK, 34 Not)
private final int MAXPacketSize = 28 ;
private byte[] writeBuff ;
private byte[] readBuff ;
private boolean bRefreshRead =false ;
private boolean bRefreshExit =false ;
//为真时退出监视Usb读取的线程
private boolean bUsbReadExit =false ;
//读线程正在运行的标记
private boolean bUsbReadRunning = false ;
//读线程锁块
private final String strUsbReadRunning = "LOCKREAD" ;
//读线程中要使用的USB请求类
private UsbRequest okUsbReq = null ;
//写未完成
private boolean bUsbWriteOK =false ;
/*
* Configuration Request Types
*/
// 还有另一种HID配置格式: 对于requestType, 分别是0x21(输出) 和 0xA1(输入),
//它表示该令牌为HID类请求(bit6 bit5=0 1)而且接口为其接收者(低5位为00001)
//0010 0001 最高位bit7为0的方向(输出--主机到设备), 低5位00001表示请求对象的接收者为接口
// bit6-bit5: 请求的类型--- 0表示标准请求, 1表示类请求, 2表示供应商提供的厂商请求, 3保留
private final int JGB_HOST_TO_DEVICEA = 0x21;
//1010 0001 最高位为1的方向(输入--设备到主机) , 低5位00001表示请求对象的接收者为接口
// bit6-bit5: 请求的类型--- 0表示标准请求, 1表示类请求, 2表示供应商提供的厂商请求, 3保留
private final int JGB_DEVICE_TO_HOSTA = 0xA1;
//另外: JGB01开发板还可用的厂商请求定义为:
//0100 0001 最高位为0的方向(输出--主机到设备), 低5位00001表示请求对象的接收者为接口
//它表示该控制传输为厂商请求(bit6 bit5=1 0), 而且接口为其接收者(低5位为00001)
private final int JGB_HOST_TO_DEVICEB = 0x41;
//1100 0001 最高位为1的方向(输入--设备到主机) , 低5位00001表示请求对象的接收者为接口
//它表示该控制传输为厂商请求(bit6 bit5=1 0), 而且接口为其接收者(低5位为00001)
private final int JGB_DEVICE_TO_HOSTB = 0xc1;
//JGB01开发板请求码
private final int JGB_STATUS_REQUEST_CODE =0xfe ;
//JGB01开发板只有一个接口0
private final int JGB_PORT_INDEX =0 ;
//为真表示用控制传输模式发送请求,为假表示用UsbRequest异步模式发送请求(此为默认模式)
private boolean bControlMode = false ;
//当前的命令字
private String strCurrSend ="" ;
//命令字需要重复发送的次数
private int nCurrSend = 0 ;
//命令字的ID
private int nCurrFlag = 0;
//为真则表示每间隔5秒就自动传输一次
private boolean bAutoFireFlag = false ;
//对JGB01开发板的控制传输操作后的返回值存放点(最多MAXPacketSize字节)
private byte[] jgbBuffer;
private boolean bRefreshJGB = false ;
private int nJGBLength =0;
@Override
protected void onStart() {
super.onStart();
}
@Override
protected void onResume() {
super.onResume();
//开始时的输入焦点
spChip.setFocusable(true);
spChip.setFocusableInTouchMode(true);
}
@Override
protected void onDestroy() {
super.onDestroy();
//反注册广播接收器
unregisterReceiver(broadcastReceiver);
//需关闭端口并且退出读线程
closeUsbPortA();
//退出界面定时更新USB读取到的数据之线程
bRefreshExit =true ;
//退出监视Usb读取的线程
bUsbReadExit =true ;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
editRead = (EditText) findViewById(R.id.editRead);
editSend = (EditText) findViewById(R.id.editSend);
tvRead = (TextView) findViewById(R.id.tvRead);
tvAuthor = (TextView) findViewById(R.id.tvAuthor);
buttSend = (Button) findViewById(R.id.buttSend);
buttClear = (Button) findViewById(R.id.buttClear);
buttOpen = (Button) findViewById(R.id.buttOpen);
buttClose = (Button) findViewById(R.id.buttClose);
buttLedxOn = (Button) findViewById(R.id.buttLedxOn);
buttLedxOff = (Button) findViewById(R.id.buttLedxOff);
buttTempOn = (Button) findViewById(R.id.buttTempOn);
buttTempOff =(Button) findViewById(R.id.buttTempOff);
spChip = (Spinner) findViewById(R.id.spChip);
spBaud = (Spinner) findViewById(R.id.spBaud);
//注册按键动作监听器
//入参为: View.OnClickListener 的接口实例,即 this
buttSend.setOnClickListener(this);
buttClear.setOnClickListener(this);
buttOpen.setOnClickListener(this);
buttClose.setOnClickListener(this);
buttLedxOn.setOnClickListener(this);
buttLedxOff.setOnClickListener(this);
buttTempOn.setOnClickListener(this);
buttTempOff.setOnClickListener(this);
//取得或失去焦点时的监听
editSend.setOnFocusChangeListener(this);
spChip.setOnFocusChangeListener(this);
spBaud.setOnFocusChangeListener(this);
buttTempOff.setOnFocusChangeListener(this);
//不能弹出输入键盘
editRead.setKeyListener(null);
//不能取得输入焦点
editRead.setFocusable(false);
editRead.setFocusableInTouchMode(false);
buttClose.setEnabled(false);
//修改标题栏文字内容
actionBar=getSupportActionBar();
if(actionBar != null){
actionBar.setTitle("USB-HID调试器");
}
//设备选择
//创建一个数组适配器
adapterChip = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, chipList);
//设置下拉列表框的下拉选项样式
adapterChip.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spChip = (Spinner)findViewById(R.id.spChip);
spChip.setAdapter(adapterChip);
//行缓冲区,每一行最多1024字节,超过将被忽略
lineBuff = new byte[1024];
Arrays.fill(lineBuff, (byte) 0);
//此行的真实长度
nLineActLen =0 ;
//最大包长度MAXPackageSize
readBuff =new byte[MAXPacketSize];
writeBuff =new byte[MAXPacketSize];
//对JGB01开发板的控制传输操作后的返回值存放点(最多MAXPacketSize字节)
jgbBuffer=new byte[MAXPacketSize];
bRefreshJGB = false ;
//清空退出USB读取线程的标记
bUsbReadExit =false ;
bRefreshRead =false ;
bRefreshExit =false ;
//strRefreshALL ="";
//开启定时界面更新线程
refreshReadData();
//加入几种操作符
baudList.clear();
baudList.add("Select-下拉选择");
baudList.add("SetLedx:1-点亮");
baudList.add("SetLedx:0-熄灭");
baudList.add("GetTemp:1-温控");
baudList.add("GetTemp:0-停止");
baudList.add("GetStatus:1-取设备状态");
baudList.add("GetEPStatus:1-取所有端点状态");
baudList.add("ClrInfo:1-清空显示并切换到控制传输");
//创建一个数组适配器
adapterBaud = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, baudList);
//设置下拉列表框的下拉选项样式
adapterBaud.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spBaud = (Spinner)findViewById(R.id.spBaud);
spBaud.setAdapter(adapterBaud);
//注册监听器
spChip.setOnItemSelectedListener(this);
//注册监听器
spBaud.setOnItemSelectedListener(