美文网首页Android 开发经验集Android技术知识Android开发经验谈
Android 串口通信笔记2 调试工具分析 工具类实现分析、项

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

作者: silencefun | 来源:发表于2018-01-25 18:16 被阅读150次

    1.调试工具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项目结构

    从结构中可以看出来 是把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 对象以及刷新线程并启动。

    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移植实现

    在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

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

    相关文章

      网友评论

        本文标题:Android 串口通信笔记2 调试工具分析 工具类实现分析、项

        本文链接:https://www.haomeiwen.com/subject/ztouaxtx.html