安卓设备通过USB串口与STM32单片机通讯之二
本博文系JGB联合商务组的原创作品,引用请标明出处。
本博文接续上一篇的末尾章节。
(二) APP的JAVA代码部分(使用Android Studio 4.1环境开发)
本APP(USB串口调试器)在开发中引入了 mike wakerly ([email protected])编写的USB串口开源驱动包.具体的开源项目可查看:
https://github.com/mik3y/usb-serial-for-android。
得益于这个工作流畅的驱动包,本APP在实际使用中稳定可靠,在此我谨向这位无私奉献的开源工作者致谢。
Forks并下载这个开源包后得到相关驱动文件。
以下是这个driver驱动包复制到本项目中的具体位置,它和我的唯一的主活动MainActivity是在同一层的,且包内一共有15个源文件。
由于我的包名叫: com.example.hohousbserial ,因此此驱动包内所有的源文件的第一行都需手动修改包名为:
package com.example.hohousbserial.driver;
其它的如作者名称等都不应改变。
例如下面的开源驱动包内 CommonUsbSerialPort.java 文件的第一行就是已修改了包名的样式。
package com.example.hohousbserial.driver;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbEndpoint;
import android.hardware.usb.UsbRequest;
import android.util.Log;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.EnumSet;
/**
* A base class shared by several driver implementations.
*
* @author mike wakerly ([email protected])
*/
public abstract class CommonUsbSerialPort implements UsbSerialPort {
private static final String TAG = CommonUsbSerialPort.class.getSimpleName();
private static final int DEFAULT_WRITE_BUFFER_SIZE = 16 * 1024;
private static final int MAX_READ_SIZE = 16 * 1024; // = old bulkTransfer limit
//......
}
然后在我的源代码文件 MainActivity.java 中这样引入驱动包即可:
import com.example.hohousbserial.driver. ;*
成功地引入驱动包这个最重要的工作完成后我们就可以正式开工了。
首先依照通讯工程开发惯例,我们首先定义通讯协议,Android设备在这里是USB Host端,负责指令发送,简称为发送端。而JGB01开发板是USB Device端,负责指令的接收和执行,简称为接收端。
双方约定都以字符串加上换行回车符作为一条完整的通讯消息标记。
另外接收端在收到发送端的一条指令后都会回发确认消息,格式如下:
Received:+ 发送端指令
简单的通讯协议如下表:
序号 | 发送端发送 | 接收端动作 |
---|---|---|
1 | SetLedx:1 | 点亮蓝灯 |
2 | SetLedx:0 | 熄灭蓝灯 |
3 | GeTemp:1 | 每隔5S取回温度值 |
4 | GetTemp:0 | 停止取回温度值 |
(表2-1)
协议虽简单但是很重要。以后无论是Android设备的Java代码还是STM32F103单片机的C代码都将围绕着这个通讯协议的实现而进行编程。
首先上APP布局文件:layout\activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/io_cfg"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<!-- 下拉列表框 -->
<LinearLayout
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:layout_marginTop="5dp"
android:layout_marginBottom="2dp"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<TextView
android:id="@+id/tvChip"
android:text="芯片类"
android:layout_marginRight="15dp"
android:textColor="@color/bright_foreground_material_light"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<Spinner
android:layout_width="0dp"
android:layout_weight="5"
android:layout_height="wrap_content"
android:background="@color/accent_material_dark"
android:layout_marginRight="5dp"
android:id="@+id/spChip" />
</LinearLayout>
<LinearLayout
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:layout_marginTop="2dp"
android:layout_marginBottom="2dp"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<TextView
android:id="@+id/tvBaud"
android:text="波特率"
android:layout_marginRight="15dp"
android:textColor="@color/bright_foreground_material_light"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<Spinner
android:layout_width="0dp"
android:layout_weight="5"
android:layout_height="wrap_content"
android:background="@color/accent_material_dark"
android:layout_marginRight="5dp"
android:id="@+id/spBaud" />
</LinearLayout>
<LinearLayout
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:layout_marginTop="2dp"
android:layout_marginBottom="2dp"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<Button
android:layout_marginRight="3dp"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:gravity="center"
android:text="关闭"
android:id="@+id/buttClose" />
<Button
android:layout_marginLeft="3dp"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:gravity="center"
android:text="打开"
android:id="@+id/buttOpen" />
</LinearLayout>
<LinearLayout
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:layout_marginTop="2dp"
android:layout_marginBottom="2dp"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<Button
android:id="@+id/buttLedxOn"
android:text="点亮"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<Button
android:id="@+id/buttLedxOff"
android:text="熄灭"
android:layout_marginLeft="4dp"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<Button
android:id="@+id/buttTempOn"
android:text="温控"
android:layout_marginLeft="4dp"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<Button
android:id="@+id/buttTempOff"
android:text="停止"
android:layout_marginLeft="4dp"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
</LinearLayout>
<LinearLayout
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<!-- textCursorDrawable: 显示红色的光标 -->
<EditText
android:layout_marginRight="0dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:cursorVisible="true"
android:textCursorDrawable="@drawable/textcursorcolor"
android:id="@+id/editSend"
android:text="GetTemp:1"
android:background="@color/accent_material_dark"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3"
/>
<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/buttSend" />
</LinearLayout>
<LinearLayout
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="2dp"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<TextView
android:id="@+id/tvRead"
android:text="收到数据"
android:textColor="@color/bright_foreground_material_light"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="start"
/>
<TextView
android:id="@+id/tvAuthor"
android:text="(作者:JGB联合商务组)"
android:textColor="@color/bright_foreground_material_light"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
/>
</LinearLayout>
<EditText
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:layout_marginTop="2dp"
android:layout_marginBottom="5dp"
android:id="@+id/editRead"
android:text="[停止-翻页复用键查看此内容]\n[发送框转义: \\n=换行 \\r=回车]\n"
android:inputType="none"
android:background="@color/accent_material_dark"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="top"
/>
</LinearLayout>
以上是一个简单的线性布局,其中有一个红色光标的定义如下:
drawable\textcursorcolor.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
<solid android:color="#FFFF0000" />
<size android:width="1dp"/>
</shape>
在Android Studio 4.1中的布局效果图如下:
配置清单文件(申请的权限有点多,算是为以后的迭代升级做准备吧):
src\main\AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.hohousbserial"
android:versionCode="105"
android:versionName="1.0.5">
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" /> <!-- SD卡文件权限: sd卡内创建和删除文件权限 -->
<uses-permission android:name="android.permission.READ_MEDIA_STORAGE" />
<uses-permission
android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"
tools:ignore="ProtectedPermissions" />
<uses-permission
android:name="android.permission.WRITE_SETTINGS"
tools:ignore="ProtectedPermissions" />
<uses-permission
android:name="android.permission.WRITE_MEDIA_STORAGE"
tools:ignore="ProtectedPermissions" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="安卓串口"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.HoHoUsbSerial">
<activity android:name=".MainActivity"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- 不使用静态接收广播时可将其注释掉 -->
<intent-filter>
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
</intent-filter>
<meta-data
android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
android:resource="@xml/device_filter" />
</activity>
</application>
</manifest>
几种在本布局文件中可能用到的颜色值:
values\colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="normalbackground">#FF80CBC4</color>
<color name="normalhighlight">#FF2AFFED</color>
</resources>
以下是本项目的重点: 主活动
com\example\hohousbserial\MainActivity.java
package com.example.hohousbserial;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Color;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbManager;
import android.os.Bundle;
import android.os.Handler;
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;
// 引入 mike wakerly <[email protected]> 编写的USB串口驱动库
import com.example.hohousbserial.driver.* ;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Executors;
/** 主活动代码重写了USB外设的打开,关闭,写入,读出方法
* 本类需要实现按钮动作监听类: View.OnClickListener
* 和其它几个监听类: View.OnFocusChangeListener,AdapterView.OnItemSelectedListener
*
* 本类需要引入: mike wakerly <[email protected]> 编写的开源USB串口驱动库
*
* @ 作者: JGB联合商务组 2020-12-03
*
*/
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 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串口A要用到的几个对象组合
private UsbSerialDriver driverA = null;
private UsbDevice deviceA = null;
private UsbDeviceConnection connectionA = null;
private UsbSerialPort portA = null;
private SerialInputOutputManager usbIoManagerA= null;
private UsbPermission usbPermissionA = UsbPermission.Unknown;
private int nBaudA = 115200;
//若本设备插有多个USB串口设备且要同时打开使用,则可能要添加多组这样的变量组合来操作,例如:
//针对USB串口B(同时还需要添加:openUsbSerialB()和closeUsbSerialB()两个方法)
//private UsbSerialDriver driverB = null;
//private UsbDevice deviceB = null;
//private UsbDeviceConnection connectionB = null;
//private UsbSerialPort portB = null;
//private SerialInputOutputManager usbIoManagerB= null;
//private UsbPermission usbPermissionB = UsbPermission.Unknown;
//private int nBaudB = 115200;
private static final String INTENT_ACTION_GRANT_USB = "MY_GRANT_USB";
//接收USB设备插入后所发送消息的广播接收器
private BroadcastReceiver broadcastReceiver;
private PendingIntent penIntent = null;
//检测所有USB串口设备的探头类
private UsbSerialProber prober = 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 nCursorPos =0 ;
//[收到数据]editRead这个文本框的当前内容行数
private int nContentRows = 0;
//[收到数据]editRead这个文本框能显示的行数
private int nPageRows =1 ;
//[收到数据]editRead这个文本框的页面计数器
private int nPage =1 ;
@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);
//需关闭端口并且退出读线程
closeUsbSerialA();
}
@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);
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);
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串口调试器");
}
//芯片选择
//创建一个数组适配器
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 ;
//只装入几个常用的波特率
baudList.clear();
baudList.add("115200");
baudList.add("38400");
baudList.add("19200");
baudList.add("9600");
//创建一个数组适配器
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(this);
//更新芯片类列表
updateChipList();
//定义一个广播接收器, 当它收到特定的广播后会重新设置授权标记 usbPermission 的值
broadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(INTENT_ACTION_GRANT_USB)) {
//usbPermission = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)
//? UsbPermission.Granted : UsbPermission.Denied;
//需要判断用户是按下了: [确定] 或 [取消] 按键
if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
//标记授权成功
usbPermissionA = UsbPermission.Granted;
Log.d("USB PermissionA", "收到广播: 已在授权界面取得USB运行期授权");
} else {
//标记授权未成功
usbPermissionA = UsbPermission.Denied;
Log.d("USB PermissionA", "收到广播: 未能在授权界面取得USB运行期授权");
}
} else if (intent.getAction().equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
//此广播能正常收到
//获取此USB设备信息。
UsbDevice ud = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
int nDID = ud.getDeviceId() ;
int nVID = ud.getVendorId() ;
int nPID = ud.getProductId() ;
Log.d("USB Search", "收到广播: 刚插入USB设备->"+getChipType(nVID, nPID)+
" "+nDID+
" VID="+nVID +
" PID="+nPID);
runOnUiThread(() -> {
//加入列表框
chipList.add(getChipType(nVID, nPID) + " " +
nDID +
" VID" + nVID + "/PID" + nPID);
spChip.setSelection(chipList.size()-1);
//更新界面显示
adapterChip.notifyDataSetChanged();
});
} else if (intent.getAction().equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) {
//此广播能正常收到
//先执行关闭(因为设备突然拔掉,通讯很可能异常)
closeUsbSerialA() ;
editRead.append("串口设备异外关闭\n");
//按钮使能
buttOpen.setEnabled(true);
spChip.setEnabled(true);
spBaud.setEnabled(true);
buttClose.setEnabled(false);
UsbDevice ud = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
int nDID = ud.getDeviceId() ;
int nVID = ud.getVendorId() ;
int nPID = ud.getProductId() ;
Log.d("USB PermissionA", "收到广播: 有USB设备拔掉->"+getChipType(nVID, nPID)+
" "+nDID+
" VID="+nVID +
" PID="+nPID);
runOnUiThread(() -> {
//移出列表框
String strDID =" "+nDID + " ";
String strRow ="";
for (int i=0; i<chipList.size(); i++){
strRow = chipList.get(i) ;
if (strRow.indexOf(strDID)>0) {
chipList.remove(i);
break;
}
}
//没有任何芯片,加入一个空行
if (chipList.size() == 0) chipList.add(" ") ;
spChip.setSelection(0);
//更新界面显示
adapterChip.notifyDataSetChanged();
});
}
}
};
//运行期授权的意图
//获取一个能处理自定义广播发送的意图penIntent, 后面将通过penIntent发送一个自定义广播(类似调用sendBroadcast()方法) INTENT_ACTION_GRANT_USB 给广播接收器broadcastReceiver
penIntent = PendingIntent.getBroadcast(this, 0, new Intent(INTENT_ACTION_GRANT_USB), 0);
//意图过滤器filter
IntentFilter filter = new IntentFilter();
filter.addAction(INTENT_ACTION_GRANT_USB);
filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
//注册此广播接收器,此广播接收器将只处理上面的filter定义的广播消息
registerReceiver(broadcastReceiver, filter);
}
//如果需要,可在这里处理机顶盒或电视设备的遥控器按键
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_ENTER: //确定键enter
case KeyEvent.KEYCODE_DPAD_CENTER:
//Log.d("Key","enter");
break;
case KeyEvent.KEYCODE_BACK: //返回键
//Log.d("Key","back");
//改变消息流向
//由于返回键会退出,如果不需要父级处理该退出则直接返回真
//return true;
break;
case KeyEvent.KEYCODE_SETTINGS: //设置键
//Log.d("Key","setting");
break;
case KeyEvent.KEYCODE_DPAD_DOWN: //向下键
/* 有时候会触发两次,所以要判断一下按下: KeyEvent.ACTION_DOWN 时才触发
* 按键松开:KeyEvent.ACTION_UP 不需处理
*/
if (event.getAction() == KeyEvent.ACTION_DOWN){
//Log.d("Key","down");
}
break;
case KeyEvent.KEYCODE_DPAD_UP: //向上键
//Log.d("Key","up");
break;
case KeyEvent.KEYCODE_0: //数字键0
//Log.d("Key","0");
break;
case KeyEvent.KEYCODE_DPAD_LEFT: //向左键
//Log.d("Key","left");
break;
case KeyEvent.KEYCODE_DPAD_RIGHT: //向右键
//Log.d("Key","right");
break;
default:
//Log.d("Key","此按键值为: " +keyCode);
break;
}
return super.onKeyDown(keyCode, event);
}
//更新芯片类列表
private void updateChipList(){
int nDID =0 ;
int nVID =0 ;
int nPID =0 ;
//取回USB设备管理器
usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
//芯片列表先清空
chipList.clear();
for(UsbDevice v : usbManager.getDeviceList().values()) {
nDID = v.getDeviceId() ;
nVID = v.getVendorId() ;
nPID = v.getProductId() ;
Log.d("USB Search", "USB设备名称: "+v.getDeviceName()+
" DeviceID="+nDID+
" VID="+nVID +
" PID="+nPID
);
//加入列表框
chipList.add(getChipType(nVID, nPID) +" "+
nDID +
" VID"+nVID +"/PID"+nPID);
}
//没有任何芯片,加入一个空行
if (chipList.size() == 0) chipList.add(" ") ;
//默认显示第一行
spChip.setEnabled(true);
spBaud.setEnabled(true);
spChip.setSelection(0);
spBaud.setSelection(0);
}
//从一个设备的VID和PID中找到其对应的芯片类型
// 暂列出三种国内常见的USB转串口芯片,可参考 device_filter.xml 文件添加
private String getChipType(int nVID, int nPID){
String strChip = "Unknown" ;
switch(nVID){
case 4292 :
if ( (nPID == 60000) || (nPID == 60016) || (nPID == 60017) )
strChip = "CP210x" ;
break ;
case 1659 :
if (nPID == 8963)
strChip = "PL2303" ;
break ;
case 6790 :
if ( (nPID == 21795) || (nPID == 29987) )
strChip = "CH34x" ;
break ;
}
return strChip;
}
//从一个被选中的列表框字符串中找到一个可用的USB串口设备
//入参格式类似于:CP21xx 1002 VID4292/PID60000(空格分隔的三个字符串)
private UsbDevice getDeviceFromDetail(String strDetail){
UsbDevice ud = null ;
//移除串中的空值,[ ]内是一个空格,而+表示允许多个空格。
String[] ss= strDetail.split("[ ]+") ;
if (ss.length > 2){
//第二个字符串是设备ID
int nDeviceId = Integer.parseInt(ss[1]);
if (usbManager == null)
usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
for(UsbDevice v : usbManager.getDeviceList().values()) {
if (v.getDeviceId() == nDeviceId){
ud = v;
break ;
}
}
}
return ud ;
}
//关闭USB串行口A
private void closeUsbSerialA(){
//需要退出读线程
if (usbIoManagerA !=null){
usbIoManagerA.stop();
}
//需关闭端口
if (portA.isOpen()){
try {
portA.close();
} catch (IOException e) {
e.printStackTrace();
}
}
deviceA = null ;
}
//打开USB串行口A,成功返回真 (可能需要运行期授权)
private void openUsbSerialA(){
//直接创建并启动一个匿名线程接口类的实例
new Thread(new Runnable() {
@Override
public void run() {
try {
//执行打开USB串行口A的代码
if ( execUsbSerialA() ) {
//UI界面的按钮使能
runOnUiThread(() -> {
editRead.append("USB串口初始化和打开成功!\n");
if ((portA != null) && portA.isOpen()) {
buttOpen.setEnabled(false);
spChip.setEnabled(false);
spBaud.setEnabled(false);
buttClose.setEnabled(true);
}
});
} else {
runOnUiThread(() -> {
editRead.append("USB串口初始化和打开失败!\n");
});
}
} catch (Exception e) {
e.printStackTrace();
}
} //public void run() ... end
}).start();
}
//打开USB串行口A,成功返回真 (可能需要运行期授权)
//本代码需放在一个线程内执行
private boolean execUsbSerialA()
{
boolean bSuccess = false ;
String strTemp = (String) spChip.getSelectedItem();
strTemp=strTemp.trim();
//必须在设备列表框中选中可用设备
if ( (strTemp.indexOf("Unknown")>=0 ) || (strTemp == null) ) {
runOnUiThread(() -> {
editRead.append("必须在芯片类选中一台可用设备!\n");
});
return false ;
}
deviceA =getDeviceFromDetail(strTemp);
if ( deviceA == null ){
runOnUiThread(() -> {
editRead.append("此芯片类设备尚未有相应的驱动!\n");
});
return false ;
}
// Find all available drivers from attached devices.
//usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
//List<UsbSerialDriver> availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(usbManager);
//if (availableDrivers.isEmpty()) {
if (prober == null) {
//全部受支持的USB串口设备检测表customTable
ProbeTable customTable = new ProbeTable();
//把每一个设备的PID/VID加入到设备检测表ProbeTable
//例如 VID=0x10C4=4292 , PID=0xEA60=60000 , 为CP21XX 芯片
//加入常见的几种芯片驱动
customTable.addProduct(4292, 60000, Cp21xxSerialDriver.class);
customTable.addProduct(4292, 60016, Cp21xxSerialDriver.class);
customTable.addProduct(4292, 60017, Cp21xxSerialDriver.class);
customTable.addProduct(1659, 8963, ProlificSerialDriver.class);
customTable.addProduct(6790, 21795, Ch34xSerialDriver.class);
customTable.addProduct(6790, 29987, Ch34xSerialDriver.class);
//从一个设备的VID和PID中找到对应的驱动类
// final Class<? extends UsbSerialDriver> driverClass = customTable.findDriver(vendorId, productId);
prober = new UsbSerialProber(customTable);
//availableDrivers = prober.findAllDrivers(usbManager);
}
//从一个USB设备找到其对应的驱动还可以这样做
driverA =prober.probeDevice(deviceA);
// Open a connection to the first available driver.
//driverA = availableDrivers.get(0);
//deviceA= driverA.getDevice() ;
if(usbManager.hasPermission(deviceA)) {
Log.d("USB PermissionA", "已取得此USB设备的授权: " + deviceA);
//打开USB串行口A设备及对应端口
execThisDevicePortA() ;
bSuccess =true ;
} else {
Log.d("USB PermissionA", "未取得此USB设备的授权: " + deviceA);
//需取得USB管理权限 : 运行期授权
//这里将通过penIntent发送一个广播INTENT_ACTION_GRANT_USB 给广播接收器broadcastReceiver
usbPermissionA = UsbPermission.Requested ;
usbManager.requestPermission(deviceA, penIntent);
//这里应该等待授权界面最多30秒后才关闭......
int timeout = 0 ;
while (timeout>30){
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
timeout += 1 ;
if ((usbPermissionA == UsbPermission.Granted) || (usbPermissionA == UsbPermission.Denied))
break ;
}
//授权OK了,才能打开此设备及对应端口
if (usbPermissionA == UsbPermission.Granted){
Log.d("USB PermissionA", "已取得此USB设备的运行期授权: " + deviceA);
//打开USB串行口A设备及对应端口
execThisDevicePortA() ;
bSuccess =true ;
} else {
Log.d("USB PermissionA", "在界面操作中仍未取得此USB设备的运行期授权: " + deviceA);
bSuccess =false ;
}
}
return bSuccess ;
}
//打开USB串行口A设备及对应端口
private void execThisDevicePortA(){
//先打开设备连接
connectionA = usbManager.openDevice(deviceA);
if (connectionA != null) {
Log.d("USB OpenDeviceA", "打开此USB设备OK: " + deviceA);
// Most devices have just one port (port 0)
portA = driverA.getPorts().get(0);
//打开设备的端口并置定波特率
try {
portA.open(connectionA);
portA.setParameters(nBaudA, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE);
Log.d("USB OpenPortA", "打开此USB设备的端口OK: " + deviceA);
//回调方式读取 : 实际使用时应该用此方法
//第二个入参为: SerialInputOutputManager.Listener 的接口实例,即 portAListener
usbIoManagerA = new SerialInputOutputManager(portA, portAListener);
usbIoManagerA.stop();
//读取超时不要设置太大了,否则读线程被阻塞的时间很长,读取响应很慢
//但也不要设为0,因为0表示无限等候读取
//在115200波特率下,每秒大约能读取13KB字节,而一般发送端的数据包都不会超过1KB大小
//因此把等候时间设为100ms是可行的
usbIoManagerA.setReadTimeout(100);
Executors.newSingleThreadExecutor().submit(usbIoManagerA);
Log.d("USB RunReadThreadA", "运行此USB设备的读取线程OK: " + deviceA);
} catch (IOException e) {
e.printStackTrace();
Log.d("USB OpenPortA", "打开此USB设备的端口Error: " + deviceA);
}
} else {
Log.d("USB OpenDeviceA", "打开此USB设备Error: " + deviceA);
}
}
//在指定的usb串口上发送字符串
private void writeString(UsbSerialPort port , String strOut){
//本APP的接收和发送都采用GBK编码,与通讯另一端的单片机或其它windows串口控制台软件一致.
//这样确保了汉字的发送也是正常的
byte[] bytesOut= new byte[0];
try {
bytesOut = strOut.getBytes("GBK");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
writeBytes(port,bytesOut) ;
}
//在指定的usb串口上发送字节流
private void writeBytes(UsbSerialPort port ,byte[] bytesOut){
if (bytesOut.length > 0){
//直接创建并启动一个匿名线程接口类的实例
new Thread(new Runnable() {
@Override
public void run() {
try {
if( (port != null) && (port.isOpen()) ) {
int cnt=port.write(bytesOut, 2000);
String strMsg = "USB串口"+port.getPortNumber()+"已发送总字节数: "+cnt +"\n";
Log.d("USB Write", strMsg );
//UI界面的更新
runOnUiThread(() -> {
editRead.append(strMsg);
});
} else {
//UI界面提示
runOnUiThread(() -> {
editRead.append("USB串口" + port.getPortNumber() + "对象为空或端口尚未打开\n");
});
}
} catch (Exception e) {
e.printStackTrace();
}
} //public void run() ... end
}).start();
}
}
//发送R.id.editSend文本编辑框的字符串
private void sendEditTextString()
{
//自动在最后面添加换行符和回车符 : 0x0A=换行\n , 0x0D=回车\r
if ((portA !=null ) && portA.isOpen()) {
String strTemp = editSend.getText().toString();
//先替换字符串中的转义符: \n和\r
strTemp=strTemp.replace("\\r","\r") ;
strTemp=strTemp.replace("\\n","\n") ;
//再在最后添加换行符和回车符
strTemp += "\n\r";
writeString(portA, strTemp);
editRead.append("已发送: "+editSend.getText().toString()+"\n");
}
else
{
editRead.setText("请先打开串口设备\n");
}
}
/**
* 返回EditText控件在某一行之前的字符总数
* @param editText 需要统计字符个数的EditText
* @param nLine 指定的行数
* @return :int 此行之前的总字符个数
*/
private int getCharsCount(EditText editText,int nLine) {
int cnt =0;
String s=editText.getText().toString();
String[] ss =s.split("\n");
if (ss.length <= nLine){
cnt =s.length() ;
} else {
for (int i=0; i<nLine;i++){
cnt +=ss[i].length() ;
}
}
return cnt;
}
/**
* 判定EditText控件垂直方向是否可以滚动
* @param editText=需要判断的EditText
* @return true=滚动 false=不可以滚动
*/
private boolean isVerticalScroll(EditText editText) {
//控件的能滚动到的最顶点,在此顶点之外不能绘制任何像素
//垂直滚动的距离 : 垂直能滚动时该值大于0,不滚动时为0
int scrollY = editText.getScrollY();
//控件内容的总高度
int scrollContent = editText.getLayout().getHeight();
//实际控件显示的可用高度
int scrollAvailable = editText.getHeight() - editText.getCompoundPaddingTop() -editText.getCompoundPaddingBottom();
//控件内容总高度与实际控件显示的可用高度的差值,如果该差值大于1个像点时也表示需要垂直滚动才能看到被遮挡的内容
int scrollDifference = scrollContent - scrollAvailable;
nContentRows = editText.getLineCount();
int nMaxLines= editText.getMaxLines();
int nLineHeight=editText.getLineHeight();
//这个文本框每页能显示的行数
if (nLineHeight>0) nPageRows = (scrollAvailable / nLineHeight)-1 ;
if (nPageRows<=0) nPageRows =1;
Log.d("ScrollY: ", "此文本框当前内容总行数为: " +nContentRows+" 每页行数为: " +nPageRows);
Log.d("ScrollY: ", "允许显示最大行数为: " + nMaxLines + " 行高为: "+nLineHeight);
Log.d("ScrollY: ", "内容Y值为: " +scrollContent+ " 控件Y值为: " + scrollAvailable);
Log.d("ScrollY: ", "滚动Y的最顶点为: " +scrollY+ " 内容Y超出控件为: " + scrollDifference);
if(scrollDifference <=1 ) {
return false;
}
return (scrollY > 0) || ( 1 < scrollDifference) ;
}
// ***************************** 下面实现本类的几个监听器接口 ************************************
//实现本类的View.OnClickListener接口要求实现的唯一方法: onClick(View v)
public void onClick(View v) {
//让editRead[收到数据]这个文本框的光标移到最后面(默认)
if ( v.getId() != R.id.buttTempOff ) {
int nLen = editRead.getText().length();
editRead.setSelection(nLen);
}
switch (v.getId()) {
case R.id.buttSend:
//在USB串行口A上发送R.id.editSend编辑框的数据
sendEditTextString();
break;
case R.id.buttOpen:
//打开USB串行口A(可能需要运行期授权)
openUsbSerialA() ;
editRead.setText("");
break;
case R.id.buttClose:
//关闭USB串行口A
closeUsbSerialA() ;
editRead.append("串口设备已关闭\n");
//按钮使能
buttOpen.setEnabled(true);
spChip.setEnabled(true);
spBaud.setEnabled(true);
buttClose.setEnabled(false);
break;
case R.id.buttLedxOn:
//点亮蓝色LED的指令串
editSend.setText("SetLedx:1");
//在USB串行口A上发送R.id.editSend编辑框的数据
sendEditTextString();
break;
case R.id.buttLedxOff:
//熄灭蓝色LED的指令串
editSend.setText("SetLedx:0");
//在USB串行口A上发送R.id.editSend编辑框的数据
sendEditTextString();
break;
case R.id.buttTempOn:
//连续取回环境温度的指令串
//测试插入换行回车符 : 0x0A=换行\n , 0x0D=回车\r
//editSend.setText("GetTemp:1\\n\\r");
editSend.setText("GetTemp:1");
//在USB串行口A上发送R.id.editSend编辑框的数据
sendEditTextString();
break;
case R.id.buttTempOff:
String strOld =editSend.getText().toString().trim();
//以下实现按钮复用: 反复地按这个停止键可以让[收到数据]这个文本框上下滚动,以便用户查看其全部的内容
//需要用到如下变量:
//[收到数据]editRead这个文本框的当前内容行数 : nContentRows
//[收到数据]editRead这个文本框能显示的行数 : nPageRows
//[收到数据]editRead这个文本框的页面计数器: nPage
if ( strOld.equals("GetTemp:0") && isVerticalScroll(editRead) ) {
//Log.d("VerticalScroll", "editRead的垂直滚动条出现了!" );
//暂时复用为翻页键,失去焦点后会还原为停止键
//总页数
if (nPageRows <=0) nPageRows=1;
int nPageAll = nContentRows / nPageRows ;
if ((nContentRows % nPageRows) != 0) nPageAll +=1 ;
buttTempOff.setText("翻页");
//tvRead.setText(1+"/"+nPageAll+"页");
int nChars = 0 ;
int nLine = 1 ;
if (nPage > 1) {
nLine = nPage * nPageRows ;
nChars = getCharsCount(editRead, nLine);
}
tvRead.setText(nPage+"/"+nPageAll+"页");
editRead.setSelection(nChars);
if (nPage < nPageAll) {
//页面计数增加
nPage += 1;
} else {
nPage = 1;
}
} else {
nPage =1 ;
buttTempOff.setText("停止");
tvRead.setText("收到数据:");
//停止取回环境温度的指令串
editSend.setText("GetTemp:0");
//在USB串行口A上发送R.id.editSend编辑框的数据
sendEditTextString();
}
break;
default:
}
}
//USB串口A上的读取线程要回调的接口
SerialInputOutputManager.Listener portAListener = new SerialInputOutputManager.Listener() {
//本类的SerialInputOutputManager.Listener 接口中的两个方法
@Override
public void onNewData(byte[] data) {
//有换行符的完整的一行字符串
String strALL = "";
//当前读到的数据块的字节数
int cnt=data.length ;
try {
//在这里收到单片机发来的汉字字符串时有时会出现乱码,本博主实际按以下思路得到完美解决:
//为了防止字符串有汉字时显示乱码(因为串口有时读到字节刚好是把某个GBK汉字的两个编码分成两个数据块读入了),
// 因此我们需要按换行符来处理每一行文字
//即: 收到的字节流先添加到缓冲区里,然后检查此缓冲区里是否出现了换行符,如果是则把此行读出来,
//然后将此行按GBK处理(附注: STM32单片机发送过来的是GBK编码)
//处理完后需同时清空此行缓冲区lineBuff
//当前字节先无条件放入lineBuff,逐个检查此字节是否为换行符,,
// 若是则处理行缓冲区lineBuff的UI更新后才清空lineBuff并且继续放入余下的字节
for (int i=0; i < cnt; i++){
//数组越界检查
//只要行缓冲区未满就无条件将当前字节数放入行缓冲区
if (nLineActLen < (lineBuff.length-1)) {
lineBuff[nLineActLen] = data[i];
nLineActLen += 1;
}
if (data[i] == (byte)('\n') ){
//只要换行符出现了,则立即处理行缓冲区,
//发送端过来的回车符不用管(回车符只为以后使用本APP发送AT指令时的兼容)
//对于单片机,发送过来的多是GB2312或GBK编码
//如果发送过来的是UTF-8编码,则应该用: strALL += new String(data, 0, data.length, "UTF-8");
strALL += new String(lineBuff, 0, nLineActLen, "GBK");
//处理完了行缓冲区后需要清空
Arrays.fill(lineBuff, (byte) 0);
nLineActLen = 0;
}
}
}
catch (Exception ee)
{
strALL = "未能识别的字符串编码: " + ee.getMessage()+"\n";
}
finally
{
if (strALL != null) {
//UI界面的更新
String finalStrALL = strALL;
runOnUiThread(() -> {
editRead.append(finalStrALL);
});
}
}
}
@Override
public void onRunError(Exception e) {
//接收线程出错了
e.printStackTrace();
}
};
//本类的OnItemSelectedListener接口要求实现的两个方法
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
String strTemp;
String strCmd;
switch (parent.getId()) {
//芯片选择
case R.id.spChip :
strTemp = (String) spChip.getItemAtPosition(position);
strTemp =strTemp.trim();
if ( (strTemp.indexOf("Unknown")>=0 )|| (strTemp == null) ){
editRead.append("驱动不存在将无法打开此设备端口\n");
deviceA =null ;
} else {
deviceA = getDeviceFromDetail(strTemp);
if (deviceA !=null)
editRead.append("已找到此USB串口设备\n");
}
break;
//波特率选择
case R.id.spBaud :
strTemp = (String) spBaud.getItemAtPosition(position);
strTemp =strTemp.trim();
nBaudA = Integer.parseInt(strTemp);
break;
}
//更新界面显示
adapterChip.notifyDataSetChanged();
adapterBaud.notifyDataSetChanged();
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
//某个控件的焦点变化时的监听器(获得焦点时高亮显示)
@Override
public void onFocusChange(View v, boolean hasFocus) {
switch (v.getId()) {
case R.id.editSend:
if (hasFocus) {
// 获得焦点
//Log.d("Focus", "editSend取得了焦点" );
//光标移到文本末尾
int len= editSend.getText().length();
if (len>0) editSend.setSelection(len);
editSend.setBackgroundColor(getResources().getColor(R.color.normalhighlight));
} else {
// 失去焦点
//Log.d("Focus", "editSend失去了焦点" );
editSend.setBackgroundColor(getResources().getColor(R.color.normalbackground));
}
break ;
case R.id.spChip:
if (hasFocus) {
// 获得焦点
//Log.d("Focus", "spChip取得了焦点" );
spChip.setBackgroundColor(getResources().getColor(R.color.normalhighlight));
} else {
// 失去焦点
//Log.d("Focus", "spChip失去了焦点" );
spChip.setBackgroundColor(getResources().getColor(R.color.normalbackground));
}
break ;
case R.id.spBaud:
if (hasFocus) {
// 获得焦点
//Log.d("Focus", "spBaud取得了焦点" );
spBaud.setBackgroundColor(getResources().getColor(R.color.normalhighlight));
} else {
// 失去焦点
//Log.d("Focus", "spBaud失去了焦点" );
spBaud.setBackgroundColor(getResources().getColor(R.color.normalbackground));
}
break ;
case R.id.buttTempOff:
if (!hasFocus) {
// 失去焦点
//Log.d("Focus", "buttTempOff失去了焦点" );
buttTempOff.setText("停止");
tvRead.setText("收到数据:");
}
break ;
}
}
// ***************************** 实现本类的几个监听器接口 end ************************************
}
对外部设备的操作和文件访问类似,不外乎四个方法: open(), close() , write() , read()
因此在我这个源码文件中主要完成了这四个方法的重写:
(1)重写了设备和端口的打开方法,只需用一个方法即可打开 [芯片类] 选择框中指定的USB串口设备,并完成运行期的授权
即: openUsbSerialA()
(2) 重写了设备和端口的关闭方法,只需用一个方法即可关闭之前打开的设备,并关闭串口接收线程
即: closeUsbSerialA()
(3)增写了在指定的usb串口上发送字符串的方法,在把字符串转换为字节流时使用了GBK编码,使得发送的汉字能被中文 Windows 系统上的串口助手软件或单片机系统的C代码正确识别。
即: writeString(UsbSerialPort port , String strOut)
写字节数组:writeBytes(UsbSerialPort port ,byte[] bytesOut) 基本保持不变。
(4)本APP同样不直接使用轮循式的Read()方法 , 而是使用了开源驱动包中提供的串口接收线程(SerialInputOutputManager)来阻塞直到读入到有效字节,然后回调本活动中的一个接口来处理读到的数据块。
即:onNewData( byte[] data)
原作者的方法较简单,即:
@Override
public void onNewData(byte[] data) {
mainLooper.post(() -> {
receive(data);
});
}
//静态方法HexDump.dumpHexString(byte[] data)主要是把读取到的字节数组转换为空格分隔的16进制字符串形式
//然后追加显示在控件 receiveText 的尾部,因此显示不了STM32单片机发回的字符串(包括中文)原值.
private void receive(byte[] data) {
SpannableStringBuilder spn = new SpannableStringBuilder();
spn.append("receive " + data.length + " bytes\n");
if(data.length > 0)
spn.append(HexDump.dumpHexString(data)+"\n");
receiveText.append(spn);
}
我重写该方法的思路是: 定义一个1024字节的行缓冲区lineBuff, 并用一个整数变量nLineActLen作计数器
指示当前已收到的真实的行长度,每次收到一个字节块data时就逐个把块中的字节(不是块复制)放入
行缓冲区lineBuff,同时检查当前字节是否为换行符。
- 若不是则继续放置下一个字节。
- 若是则立即把行缓冲区lineBuff的字节数组按GBK编码构造一个字符串并更新到界面上,同时清空
行缓冲区lineBuff和计数器nLineActLen后,把字节块data剩下的字节继续放入lineBuff。
实测使用该方法能完整地显示发送端传来的中英文字符,不会出现汉字显示乱码的问题。
//声明的两个变量如下:
//行缓冲区,每一行最多1024字节,超过将被忽略
private byte[] lineBuff ;
//此行的真实长度
private int nLineActLen =0 ;
对上面两个变量的初始化如下
protected void onCreate(Bundle savedInstanceState) {
//......
//行缓冲区,每一行最多1024字节,超过将被忽略
lineBuff = new byte[1024];
Arrays.fill(lineBuff, (byte) 0);
//此行的真实长度
nLineActLen =0 ;
//......
}
接下来,我重写的onNewData(byte[] data)方法如下:
//USB串口A上的读取线程要回调的接口
SerialInputOutputManager.Listener portAListener = new SerialInputOutputManager.Listener() {
//覆盖接口类 SerialInputOutputManager.Listener 的两个方法 :
// onNewData(byte[] data) 和 onRunError(Exception e)
@Override
public void onNewData(byte[] data) {
//有换行符的完整的一行字符串
String strALL = "";
//当前读到的数据块的字节数
int cnt=data.length ;
try {
//在这里收到单片机发来的汉字字符串时有时会出现乱码,本博主实际按以下思路得到完美解决:
//为了防止字符串有汉字时显示乱码(因为串口有时读到字节刚好是把某个GBK汉字的两个编码分成两个数据块读入了),
// 因此我们需要按换行符来处理每一行文字
//即: 收到的字节流先添加到缓冲区里,然后检查此缓冲区里是否出现了换行符,如果是则把此行读出来,
//然后将此行按GBK处理(附注: STM32单片机发送过来的是GBK编码)
//处理完后需同时清空此行缓冲区lineBuff
//当前字节先无条件放入lineBuff,逐个检查此字节是否为换行符,,
// 若是则处理行缓冲区lineBuff的UI更新后才清空lineBuff并且继续放入余下的字节
for (int i=0; i < cnt; i++){
//数组越界检查
//只要行缓冲区未满就无条件将当前字节数放入行缓冲区
if (nLineActLen < (lineBuff.length-1)) {
lineBuff[nLineActLen] = data[i];
nLineActLen += 1;
}
if (data[i] == (byte)('\n') ){
//只要换行符出现了,则立即处理行缓冲区,
//发送端传来的回车符不用管(回车符只为以后使用本APP用于发送AT指令时的兼容)
//对于单片机,发送过来的多是GB2312或GBK编码
//如果发送过来的是UTF-8编码,则应该用: strALL += new String(data, 0, data.length, "UTF-8");
strALL += new String(lineBuff, 0, nLineActLen, "GBK");
//处理完了行缓冲区后需要清空
Arrays.fill(lineBuff, (byte) 0);
nLineActLen = 0;
}
}
}
catch (Exception ee)
{
strALL = "未能识别的字符串编码: " + ee.getMessage()+"\n";
}
finally
{
if (strALL != null) {
//UI界面的更新
String finalStrALL = strALL;
runOnUiThread(() -> {
editRead.append(finalStrALL);
});
}
}
}
@Override
public void onRunError(Exception e) {
//接收线程出错了
e.printStackTrace();
}
};
由于本APP很简单,只有一个主活动代码 MainActivity.java 和一个布局文件activity_main.xml
因此创建,编译,调试你的工程项目相信是很简单的。
当APP跑起来后,你将直接面临的问题是:没有硬件设备怎么调试?真机如何识别USB串口硬件?
通讯控制是否正常?
其实前文所说的什么JGB01开发板并不是必要的,调试时我们真正需要的是USB串口桥接器(分两种电平类型,分别是USB-TTL串口桥接器和USB-RS232串口桥接器)。
本节的最后我会附上一些测试图例,希望对你有所帮助。
没有任何开发板时,可以使用如下的Android USB串口开发常用小套件来联调,自上而下这些连接件分别是
- USB延长线,用于将连接线加长些,方便调试。
- TypeC-OTG转接头,用于安卓数据口为TypeC的设备。
- microUSB-OTG转接头,用于安卓数据口为microUSB的设备。
- RXD和TXD交叉互连的USB-TTL自制连接头。
其中的关键部件是两个TX和RX交叉连接的USB-TTL自制连接头。
如果只按芯片类型来区分,市场上常见的且为本APP支持的芯片有:
- CP2102芯片(包括CP2101, CP2103, CP2104, CP2105,CP2108,CP2109等芯片系列)
- CH340芯片(包括CH341A)
- PL2303芯片
使用以上连接件我们可分别得到如下实验场景:
(1)手机和手机的串口互连
(2)手机和Windows PC机的串口互联
其中的Windows电脑已开启了一个串口调试程序(也有叫串口助手之类的,有很多选择,都可以用),我用的是JGB通讯窗口,其发送端在发送完一个字符串后会尾缀两个字节:0x0D,0x0A(换行和回车),
其它的串口助手之类的软件也有此类似设定,见下图:
(3)手机和单片机的串口互联
以下是使用PL2303芯片的USB-RS232连接线来调试一个简单AT指令: AT+CSQ 即取GSM信号质量的场景(调试该指令时我用的GSM开发板没有插入手机卡)。
AT指令发送后,在读取显示区我们看到了取回的信号质量为28格,误码率为0.
以下是APP安装在机顶盒上时的调试场景:
从以上的实验场景来看,把安卓设备和单片机开发板连接后进行APP调试,最为简单,也更符合生产环境的需要。试想一下:当我们把上面的机顶盒连接图中的JGB01开发板上的蓝灯换成光电藕合器的输入发光管,然后在光藕的输出端接上继电器驱动电路或电机伺服驱动电路,那不就是一个真正的应用系统了吗?你常见的什么饮料自动售买机,自助榨果汁机等安卓应用系统就是这样实现的。因此,我认为作为一个Java软件开发者掌握一些单片机开发知识和电路硬件知识是很有必要的。
在下一个章节中我试图讲述一些本APP使用的JGB01开发板涉及到的C代码部分(使用keil 5.1 环境开发 )
(三)STM32芯片内的C代码部分(使用keil 5.1 环境开发 )