1.调试工具ComAssistant 分析
ComAssistantAndroid 端调试工具ComAssistant 如图,处于何人之手已不可考,找到的源码是用eclipse 写的。源码见文末分享。
此串口调试工具,可以同时对四个串口读写是四个独立的线程,选定串口路径 ,Linux把每个硬件也看作是一个文件,所以都是“dev/ttyS1”这种的。
注意:官方提供的 demo 没有N-8-1( N 不奇偶校验位 8 8个数据位 1 1个停止位)的设定。
第一次根据设备终端说明或者自己尝试连接电脑打开调试助手 查看到底哪个口对应哪个路径。
2.源码分析:
Eclipse版本的从哪个资源网站下载的忘记了,不过解压看是2012年8月的,所以这里边的api 适配到10(API等级10:Android 2.3.3-2.3.7 Gingerbread 姜饼),Eclipse项目结构:
Eclipse项目结构
从结构中可以看出来 是把Android官方提供的android_serial_api 从项目包中独立出来,此源码唯一不好的是 GBK 编码的 导入Android Studio中时 乱码 要从新折腾。
SerialPortFinder与SerialPort分析:
SerialPortFinder
SerialPortFinder就是遍历获取设备上所有devices以及对应的path;
public class SerialPort {
private static final String TAG = "SerialPort";
/*
* Do not remove or rename the field mFd: it is used by native method close();
*/
private FileDescriptor mFd;
private FileInputStream mFileInputStream;
private FileOutputStream mFileOutputStream;
public SerialPort(File device, int baudrate, int flags) throws SecurityException, IOException {
/* Check access permission */
if (!device.canRead() || !device.canWrite()) {
try {
/* Missing read/write permission, trying to chmod the file */
Process su;
su = Runtime.getRuntime().exec("/system/bin/su");
String cmd = "chmod 666 " + device.getAbsolutePath() + "\n"
+ "exit\n";
su.getOutputStream().write(cmd.getBytes());
if ((su.waitFor() != 0) || !device.canRead()
|| !device.canWrite()) {
throw new SecurityException();
}
} catch (Exception e) {
e.printStackTrace();
throw new SecurityException();
}
}
mFd = open(device.getAbsolutePath(), baudrate, flags);
if (mFd == null) {
Log.e(TAG, "native open returns null");
throw new IOException();
}
mFileInputStream = new FileInputStream(mFd);
mFileOutputStream = new FileOutputStream(mFd);
}
// Getters and setters
public InputStream getInputStream() {
return mFileInputStream;
}
public OutputStream getOutputStream() {
return mFileOutputStream;
}
// JNI
private native static FileDescriptor open(String path, int baudrate, int flags);
public native void close();
static {
System.loadLibrary("serial_port");
}
}
创建了打开串口和关闭串口的本地方法,在jni中实现,给Java层调用。
主要是分析 SerialHelp和 Activity的实现逻辑,SerialHelper代码:
public abstract class SerialHelper{
private SerialPort mSerialPort;
private OutputStream mOutputStream;
private InputStream mInputStream;
private ReadThread mReadThread;
private SendThread mSendThread;
private String sPort="/dev/s3c2410_serial0";
private int iBaudRate=9600;
private boolean _isOpen=false;
private byte[] _bLoopData=new byte[]{0x30};
private int iDelay=500;
//----------------------------------------------------
public SerialHelper(String sPort,int iBaudRate){
this.sPort = sPort;
this.iBaudRate=iBaudRate;
}
public SerialHelper(){
this("/dev/s3c2410_serial0",9600);
}
public SerialHelper(String sPort){
this(sPort,9600);
}
public SerialHelper(String sPort,String sBaudRate){
this(sPort,Integer.parseInt(sBaudRate));
}
//----------------------------------------------------
public void open() throws SecurityException, IOException,InvalidParameterException{
File device = new File(sPort);
//检查访问权限,如果没有读写权限,进行文件操作,修改文件访问权限
if (!device.canRead() || !device.canWrite()) {
try {
//通过挂在到linux的方式,修改文件的操作权限
Process su = Runtime.getRuntime().exec("/system/bin/su");
//一般的都是/system/bin/su路径,有的也是/system/xbin/su
String cmd = "chmod 777 " + device.getAbsolutePath() + "\n" + "exit\n";
su.getOutputStream().write(cmd.getBytes());
if ((su.waitFor() != 0) || !device.canRead() || !device.canWrite()) {
throw new SecurityException();
}
} catch (Exception e) {
e.printStackTrace();
throw new SecurityException();
}
}
mSerialPort = new SerialPort(new File(sPort), iBaudRate, 0);
mOutputStream = mSerialPort.getOutputStream();
mInputStream = mSerialPort.getInputStream();
mReadThread = new ReadThread();
mReadThread.start();
mSendThread = new SendThread();
mSendThread.setSuspendFlag();
mSendThread.start();
_isOpen=true;
}
//----------------------------------------------------
public void close(){
if (mReadThread != null)
mReadThread.interrupt();
if (mSerialPort != null) {
mSerialPort.close();
mSerialPort = null;
}
_isOpen=false;
}
//----------------------------------------------------
public void send(byte[] bOutArray){
try
{
mOutputStream.write(bOutArray);
} catch (IOException e)
{
e.printStackTrace();
}
}
//----------------------------------------------------
public void sendHex(String sHex){
byte[] bOutArray = MyFunc.HexToByteArr(sHex);
send(bOutArray);
}
//----------------------------------------------------
public void sendTxt(String sTxt){
byte[] bOutArray =sTxt.getBytes();
send(bOutArray);
}
//----------------------------------------------------
private class ReadThread extends Thread {
@Override
public void run() {
super.run();
while(!isInterrupted()) {
try
{
if (mInputStream == null) return;
byte[] buffer=new byte[512];
int size = mInputStream.read(buffer);
if (size > 0){
ComBean ComRecData = new ComBean(sPort,buffer,size);
onDataReceived(ComRecData);
}
try
{
Thread.sleep(50);//延时50ms
} catch (InterruptedException e)
{
e.printStackTrace();
}
} catch (Throwable e)
{
e.printStackTrace();
return;
}
}
}
}
//----------------------------------------------------
private class SendThread extends Thread{
public boolean suspendFlag = true;// 控制线程的执行
@Override
public void run() {
super.run();
while(!isInterrupted()) {
synchronized (this)
{
while (suspendFlag)
{
try
{
wait();
} catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
send(getbLoopData());
try
{
Thread.sleep(iDelay);
} catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
//线程暂停
public void setSuspendFlag() {
this.suspendFlag = true;
}
//唤醒线程
public synchronized void setResume() {
this.suspendFlag = false;
notify();
}
}
//----------------------------------------------------
public int getBaudRate()
{
return iBaudRate;
}
public boolean setBaudRate(int iBaud)
{
if (_isOpen)
{
return false;
} else
{
iBaudRate = iBaud;
return true;
}
}
public boolean setBaudRate(String sBaud)
{
int iBaud = Integer.parseInt(sBaud);
return setBaudRate(iBaud);
}
//----------------------------------------------------
public String getPort()
{
return sPort;
}
public boolean setPort(String sPort)
{
if (_isOpen)
{
return false;
} else
{
this.sPort = sPort;
return true;
}
}
//----------------------------------------------------
public boolean isOpen()
{
return _isOpen;
}
//----------------------------------------------------
public byte[] getbLoopData()
{
return _bLoopData;
}
//----------------------------------------------------
public void setbLoopData(byte[] bLoopData)
{
this._bLoopData = bLoopData;
}
//----------------------------------------------------
public void setTxtLoopData(String sTxt){
this._bLoopData = sTxt.getBytes();
}
//----------------------------------------------------
public void setHexLoopData(String sHex){
this._bLoopData = MyFunc.HexToByteArr(sHex);
}
//----------------------------------------------------
public int getiDelay()
{
return iDelay;
}
//----------------------------------------------------
public void setiDelay(int iDelay)
{
this.iDelay = iDelay;
}
//----------------------------------------------------
public void startSend()
{
if (mSendThread != null)
{
mSendThread.setResume();
}
}
//----------------------------------------------------
public void stopSend()
{
if (mSendThread != null)
{
mSendThread.setSuspendFlag();
}
}
//----------------------------------------------------
protected abstract void onDataReceived(ComBean ComRecData);
}
除去一些get set方法 ,主要是 构造方法 ,打开关闭方法 以及最后一行的abstract 方法onDataReceived()和一个读的线程ReadThread 和一个发送命令线程SendThread ;在ReadThread 在接收或者叫读线程中 调用了onDataReceived()方法这样在用的时候 可以直接实现调用。
SendThread 中 自动发 的原理就是 执行while语句发送命令 线程sleep()来间隔循环,控制线程暂停和唤起用的是 wait()和notif(),所以就可以通过设定flag实现自动发送。
wait() 与 notify/notifyAll 方法必须在同步代码块(synchronized关键字)中使用.
由于 wait() 与 notify/notifyAll() 是放在同步代码块中的,因此线程在执行它们时,肯定是进入了临界区中的,即该线程肯定是获得了锁的。
当线程执行wait()时,会把当前的锁释放,然后让出CPU,进入等待状态。
当执行notify/notifyAll方法时,会唤醒一个处于等待该 对象锁 的线程,然后继续往下执行,直到执行完退出对象锁锁住的区域(synchronized修饰的代码块)后再释放锁。
ReadThread 就简单了也是while()代码块 定时sleep循环 之后 读到内容之后封装成实体对象调用抽象方法onDataReceived()传递到要实现的地方。
MyFunc是一些数据转换的静态方法,如图:
MyFunc
ComAssistantActivity的大致截图 770行
ComAssistantActivity
ComAssistantActivity中 数据比较多,但是也不难捋顺,从左侧概要中可以看出来主要是一些事件处理和两个继承类:串口控制类SerialControl 继承SerialHelper和刷新显示线程DispQueueThread
如图是Activity onCreate()是实例化四个串口控制SerialControl 对象以及刷新线程并启动。
//----------------------------------------------------串口控制类
private class SerialControl extends SerialHelper{
public SerialControl(){
}
@Override
protected void onDataReceived(final ComBean ComRecData)
{
//数据接收量大或接收时弹出软键盘,界面会卡顿,可能和6410的显示性能有关
//直接刷新显示,接收数据量大时,卡顿明显,但接收与显示同步。
//用线程定时刷新显示可以获得较流畅的显示效果,但是接收数据速度快于显示速度时,显示会滞后。
//最终效果差不多-_-,线程定时刷新稍好一些。
DispQueue.AddQueue(ComRecData);//线程定时刷新显示(推荐)
Log.e("TAG", MyFunc.ByteArrToHex(ComRecData.bRec));
/*
runOnUiThread(new Runnable()//直接刷新显示
{
public void run()
{
DispRecData(ComRecData);
}
});*/
}
}
SerialControl 继承SerialHelper,那么它的实例就可以对串口进行读写操作 并且 在onDataReceived()中实现对接收到的数据进行处理。即添加到 刷新线程的 数据源队列中:DispQueue是DispQueueThread 的实例。
//----------------------------------------------------刷新显示线程
private class DispQueueThread extends Thread{
private Queue<ComBean> QueueList = new LinkedList<ComBean>();
@Override
public void run() {
super.run();
while(!isInterrupted()) {
final ComBean ComData;
while((ComData=QueueList.poll())!=null)
{
runOnUiThread(new Runnable()
{
public void run()
{
DispRecData(ComData);//更新界面
}
});
try
{
Thread.sleep(100);//显示性能高的话,可以把此数值调小。
} catch (Exception e)
{
e.printStackTrace();
}
break;
}
}
}
public synchronized void AddQueue(ComBean ComData){
QueueList.add(ComData);
}
}
其中QueueList做为接收到的数据存放队列,LinkedList是有序的,为什么AddQueue要同步加锁呢
public synchronized void AddQueue(ComBean ComData){
QueueList.add(ComData);
}
因为LinkedList是线程不安全的,开启了四个串口控制对象如果同时add()会抛出ConcurrentModificationException异常。
while语句执行的条件LinkedList.poll()方法的含义:找到并删除表头,返回null或队列中第一个对象,还是用源码来分析LinkedList
public E poll() {
return size == 0 ? null : removeFirst();
}
/**
* Removes the first object from this {@code LinkedList}.
*
* @return the removed object.
* @throws NoSuchElementException
* if this {@code LinkedList} is empty.
*/
public E removeFirst() {
return removeFirstImpl();
}
private E removeFirstImpl() {
Link<E> first = voidLink.next;
if (first != voidLink) {
Link<E> next = first.next;
voidLink.next = next;
next.previous = voidLink;
size--;
modCount++;
return first.data;
}
throw new NoSuchElementException();
}
3.项目实现
用该eclipse项目源码 做尝试移植了一份Android Studio 3.0 的项目,几番测试通过打的包也能用,同比可以迁移到自己项目。代码分享文末;
Android Studio移植实现在main 目录下创建 jni 和jniLibs ,
0.把原Eclipse项目的android_serialport_api包复制到在main/java下。
1.把原eclipse中的libs路径下的三个平台的serial_port.so同目录复制到jniLibs下。
2.把原eclipse中的c .h 文件复制到jni并重命名为android_serialport_api_SerialPort,或者使用Terminal命令生成C的头文件自己在把代码复制进去(注意路径对应方法名,这个1应该是区分包名和下划线:Java_android_1serialport_1api_SerialPort_open)
Terminal命令
①输入cd app\src\main\java进入源码所在目录
②输入javah -jni android_serialport_api.SerialPort生成头文件
③把生成的android_serialport_api_SerialPort.h复制到jni下边(没有该目录就右键 Moudle,右键菜单中选择 New -> Folder -> JNI Folder)
④右键 jni 文件夹,右键菜单中选择New -> C/C++ Source File创建与 .h 文件同名的 .c 文件。
⑤把原Eclipse 的jni下对应的.c .h文件代码复制进去
3.在build.gradle 的android节点中添加
sourceSets.main {
jniLibs.srcDir 'src/main/jniLibs'
jni.srcDirs = []
}
上图的右侧标红部分,否则会提示
Flag android.useDeprecatedNdk is no longer supported and will be removed in the next version of Android Studio. Please switch to a supported build system.
这样就直接可以用原项目编译好的.so 注意前提是要在在 local.properties 添加 ndk 路径:
#Sat Jan 20 10:09:24 CST 2018
ndk.dir=F\:\\sdk\\ndk-bundle
sdk.dir=F\:\\sdk
其下目录有Eclipse 项目源码和Android Studio 源码 以及自己使用本机debug 密钥打包的 Android调试工具和 PC 端调试工具,
github 地址 https://github.com/silencefun/ComTest
百度云链接: https://pan.baidu.com/s/1nw37xu5 密码: qscc
如果觉得有帮助,请点个赞❤ ★,谢谢。
网友评论