Android 串口通信笔记2 调试工具分析 工具类实现分析、项目实现

Android 串口通信笔记2 调试工具分析 工具类实现分析、项目实现

1.调试工具ComAssistant 分析

ComAssistant
ComAssistant

Android 端调试工具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项目结构
Eclipse项目结构

从结构中可以看出来 是把Android官方提供的android_serial_api 从项目包中独立出来,此源码唯一不好的是 GBK 编码的 导入Android Studio中时 乱码 要从新折腾。

SerialPortFinder与SerialPort分析:


SerialPortFinder
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
MyFunc

ComAssistantActivity的大致截图 770行


ComAssistantActivity
ComAssistantActivity

ComAssistantActivity中 数据比较多,但是也不难捋顺,从左侧概要中可以看出来主要是一些事件处理和两个继承类:串口控制类SerialControl 继承SerialHelper和刷新显示线程DispQueueThread
如图是Activity onCreate()是实例化四个串口控制SerialControl 对象以及刷新线程并启动。

image.png
image.png
    //----------------------------------------------------串口控制类
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移植实现
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

原文发自简书 https://www.jianshu.com/p/d3d65230ecdb

如果觉得有帮助,请点个赞❤ ★,谢谢。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值