Android NDK—— 实战演练之物联网、车联网必知必会自己动手实现串口通信控制智能家居(五)

引言

智能家居虽然不是一个新的名词,也一直不温不火,但是并不代表它没有潜力,尤其是AI+的时代,赋予了更强大的生命力和巨大的潜力,而Android系统得益于其开放性,常常被当成智能机器人、智能硬件、智能终端等上位机的操作系统,于是为了更好地实现上位机和底层核心板的通信,“串口”应运而生,早在16年的时候我就实现过串口,不过那时候项目进度紧急用的是第三方的开源库,后面在深入了解了串口之和Linux之后发现自己写也很简单。NDK实战系列相关文章链接:

一、串口通信概述

嵌入式系统或传感器网络的很多应用和测试都需要通过PC机与嵌入式设备或传感器节点进行通信。其中,最常用的接口就是RS-232串口和并口(鉴于USB接口的复杂性以及不需要很大的数据传输量,USB接口用在这里还是显得过于奢侈)。
在这里插入图片描述
关于串口更多的基本知识请参阅Android NDK——实战演练之App端通过串口通信完成实时控制单片机上LED灯的颜色及灯光动画特效

1、串口(SerialPort)

串口通信本质上就是IO操作,一般是以16进制进行数据传输的。

1.1、物理上的概念

在智能硬件的核心开发板中可以直接用VGA (HDIM)接口(转USB)或者从开发板跳线(连到USB 转换器),这两种是串口的常见物理形象,串口只需要使用到两条线路,一条负责接收一条负责发送。

转USB 只是为了更好的测试,因为电脑无法直接识别识别串口,所以转为USB就可以在PC中检测到,再通过xShell的工具就可以捕获串口的输入输出,当然也可以使用其他转换线,比如USB转RS232串口线。

1.2、逻辑软件上的概念

Android是基于Linux的,而Linux系统是基于文件的,在Linux中一切都是文件,“串口”也不例外,只不过串口是定制化Android设备上的一种特殊的设备文件

2、获取串口的相关信息

2.1、获串口对应的路径信息

Linux中串口一般是是配置到驱动上使用的。
在这里插入图片描述
可以通过查找驱动的配置清单文件/proc/tty/drivers(drivers是文件不是文件夹):
在这里插入图片描述

因为是要找串口,所以只需要关注最后一栏描述有serial字样的dev设备文件,可能需要自己去尝试或者硬件工程师直接告诉你,找到之后中间栏就是描述这个类型驱动设备对应的路径,再进入到dev文件夹下
在这里插入图片描述
根据驱动的配置文件找到的类型,进一步筛选即可。(我的板子是**/dev/ttySAC2**)

2.2、获取串口的波特率

由硬件工程师提供的,当然也可以通过一些串口网络调试工具进行测试,常见的波特率有115200、9600等,波特率必须和底层硬件一致才可以进行通信。

二、串口通信实战

串口通信本质上就是IO操作,一般是以16进制进行数据传输的,串口通信中最重要的两个参数就是路径和波特率。

  • 根据串口的路径构造File对象
  • 获取Android设备的Root权限并改变串口文件的读写权限
  • 打开串口设备并得到其对应的FileDescriptor
  • 通过FileDescriptor去创建InputStream和OutputStream
  • 进行IO操作

1、首先定义一个串口操作管理角色

串口操作管理角色主要封装了三个功能模块:

  • 打开串口
  • 向串口发送数据
  • 接收串口的数据并回调到业务层
import android.util.Log;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * 串口核心管理类
 * @author cmo
 */
public class SerialPortManager {
    private List<PortDataInterface> observers;
    private Executor executor;
    /**
     * 缓存指令的阻塞队列,避免因为并发产生类似“粘包”的问题(插入操作比较频繁时使用LinkedBlockingQueue,查询适合使用ArrayBlockingQueue)
     */
    private LinkedBlockingQueue<byte[]> queue = new LinkedBlockingQueue<>();
    /**
     *输入流(从底层读取数据),可由打开串口后返回的fd进行创建
     */
    private FileInputStream mFileInputStream;
    /**
     * 输出流(写入流,向底层发送数据)
     */
    private FileOutputStream mFileOutputStream;

    static {
        System.loadLibrary("native-lib");
    }
    static class Holder{
        private static final SerialPortManager INSTANCE=new SerialPortManager();
    }
    private SerialPortManager() {
        observers = new ArrayList<>();
        executor = Executors.newSingleThreadExecutor();
    }
    public static SerialPortManager getInstance() {
        return Holder.INSTANCE;
    }

    /**
     * 真正去发送指令
     */
    private Runnable taskCenterRunnable=new Runnable() {
        @Override
        public void run() {
            while(!Thread.currentThread().isInterrupted()){
                byte[] content=null;
                try {
                    content=queue.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if(content!=null){
                    try {
                        mFileOutputStream.write(content);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    };

    /**
     * 采用阻塞队列缓存发送的指令码
     * @param command
     */
    public void putCommand(byte[] command) {
        try {
            queue.put(command);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 注册观察者
     * @param portDataInterface
     */
    public void regist(PortDataInterface portDataInterface) {
        if (portDataInterface != null) {
            observers.add(portDataInterface);
        }
    }

    public void unregist (PortDataInterface portDataInterface) {
        if (portDataInterface != null) {
            observers.remove(portDataInterface);
        }
    }

    /**
     * 通知观察者
     * @param content
     */
    public void notifyAll(byte[] content) {
        for (PortDataInterface portDataInterface : observers) {
            portDataInterface.onDataReceived(content);
        }
    }

    /**
     * 打开串口
     * @param path 串口路径
     * @param baudRate 波特率
     */
    public void openSerialPort(String path, int baudRate) {
        File file = new File(path);
        Helper.chmod777(file);
        FileDescriptor fd=open(path, baudRate);
        mFileInputStream = new FileInputStream(fd);
        mFileOutputStream = new FileOutputStream(fd);
        if (fd != null) {
            /**
             * 开启接收串口数据的线程{@link SerialPortReadThread }
             */
            startReadThread();
            //通过线程池执行随时发送指令到串口
            executor.execute(taskCenterRunnable);
        }else{
            throw new RuntimeException("CrazyMo:Can't open the port!!");
        }
    }

    /**
     * 启动接收串口的线程
     */
    private void startReadThread() {
        SerialPortReadThread serialPortReadThread = new SerialPortReadThread(mFileInputStream) {
            @Override
            public void onReceived(byte[] readBytes) {
                /**
                 *  TODO 根据协议栈去解析得到的数据,并根据具体业务进行分发,可以通过不同的接口返回,此次是为了通用,我不做分发逻辑了
                 *
                 * 因为不同的项目,可能分为不同的业务,但是底层串口的所有指令,都是经一个串口上报的,每一种业务只对它对应的指令感兴趣,
                 * 所以需要进行“业务的发分发”,这里我使用观察者模式实现,Activity作为观察者,流作为主题
                 */
                Log.d("CrazyMo", "onReceived: "+Helper.bytesToHex(readBytes));
                // 通知观察者 也可以自己重构定义多个观察者,一个业务对应一个观察者
                SerialPortManager.this.notifyAll(readBytes);
            }
        };
        serialPortReadThread.start();
    }

    public static class Helper {
        /**
         * 获取Root权限仅针对特定的设备
         * @param file
         * @return
         */
        public static boolean chmod777(File file) {
            if (null == file || !file.exists()) {
                return false;
            }
            try {
                //只是获取ROOT权限,如果没有root的设备是不会成功的
                Process su = Runtime.getRuntime().exec("/system/bin/su");
                // 修改文件属性为 [可读 可写 可执行]
                String cmd = "chmod 777 " + file.getAbsolutePath() + "\n" + "exit\n";
                su.getOutputStream().write(cmd.getBytes());
                if (0 == su.waitFor() && file.canRead() && file.canWrite() && file.canExecute()) {
                    return true;
                }
            } catch (IOException | InterruptedException e) {
                // 没有ROOT权限
                e.printStackTrace();
            }
            return false;
        }
        public static String bytesToHex(byte[] bytes) {
            StringBuffer sb = new StringBuffer();
            for(int i = 0; i < bytes.length; i++) {
                String hex = Integer.toHexString(bytes[i] & 0xFF);
                if(hex.length() < 2){
                    sb.append(0);
                }
                sb.append(hex);
            }
            return sb.toString();
        }
    }

    /**
     * 本质就是打开指定路径下的File并设置串口属性生成Linux下的句柄,再反射生成Java层的句柄
     * @param path
     * @param baudRate
     * @return
     */
    public native FileDescriptor open(String path, int baudRate);
}

2、定义一个全周期的线程负责接收串口上传的数据并把数据通过接口回调出去

/**
 * 开启一个线程负责接收底层串口发送的数据
 * @author cmo
 */
public abstract class ReceiveDataThread extends Thread {
    private InputStream mInputStream;
    /**
     * 获取到的数据缓存到byte[]中
     */
    private byte[] mReadBuffer;

    public ReceiveDataThread(InputStream mInputStream) {
        this.mInputStream = mInputStream;
        this.mReadBuffer = new byte[1024];
    }

    public boolean isInterrupted=false;

    public void setInterrupted(boolean interrupted) {
        isInterrupted = interrupted;
    }

    @Override
    public void run() {
        while (!isInterrupted) {
            try {
                int size = mInputStream.read(mReadBuffer);
                if (size<=0) {
                    return;
                }
                byte[] readBytes = new byte[size];
                System.arraycopy(mReadBuffer, 0, readBytes, 0, size);
                onReceived(readBytes);
            } catch (IOException e) {
                e.printStackTrace();
                return;
            }
        }
    }

    /**
     * TODO 提供给应用层的数据回调接口
     * @param readBytes
     */
    public abstract void onReceived(byte[] readBytes) ;
}

3、定义一个回调给应用层的接收串口数据的接口

回调给应用层接收串口中的数据,比如需要在Activi中接收则需要实现此接口。

/**
 * @author cmo
 */
public interface PortDataReceived{
    /**
     * 数据接收
     * @param bytes 接收到的数据16进制数据
     */
    void onDataReceived(byte[] bytes);
}

4、使用native 代码打开串口

#include <jni.h>
#include <string>
#include <termios.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <jni.h>
#include "android/log.h"
static const char *TAG="CrazyMo";

#define LOGE(fmt, args...) __android_log_print(ANDROID_LOG_ERROR, TAG, fmt, ##args)
extern "C"
JNIEXPORT jobject JNICALL
Java_com_crazymo_serialprot_SerialPortManager_open(JNIEnv *env, jobject instance,
                                                          jstring path_, jint baudRate) {
    const char *path = env->GetStringUTFChars(path_, 0);
    jobject mFileDescriptor;
    speed_t speed = B115200;
    int fd = open(path, O_RDWR);
    if (fd == -1)
    {
        LOGE("打开失败");
        return NULL;
    }
    struct termios cfg;
     //获取串口属性
    if (tcgetattr(fd, &cfg))
    {
        close(fd);
        return NULL;
    }
    /**
     * 将串口设置为原始模式并让fd(对串口可度可写),在原始模式下所有的输入数据以字节为单位进行处理
     * 且终端不可回显,所有特定的终端输入/输出模式不可用
     */
    cfmakeraw(&cfg);
    //设置串口读取波特率
    cfsetispeed(&cfg, speed);
    //设置串口写入波特率
    cfsetospeed(&cfg, speed);
    /**
    *   TCSANOW:不等数据传输完毕就立即改变属性。
        TCSADRAIN:等待所有数据传输结束才改变属性。
        TCSAFLUSH:清空输入输出缓冲区才改变属性。
        注意:当进行多重修改时,应当在这个函数之后再次调用 tcgetattr() 来检测是否所有修改都成功实现。
    */
    if (tcsetattr(fd, TCSANOW, &cfg))
    {
        close(fd);
        LOGE("设置属性失败");
        return NULL;
    }
    //根据Linux的文件句柄去反射创建一个Java的文件句柄
    jclass cFileDescriptor = env->FindClass( "java/io/FileDescriptor");
    jmethodID iFileDescriptor = env->GetMethodID( cFileDescriptor, "<init>", "()V");
    jfieldID descriptorID = env->GetFieldID(cFileDescriptor, "descriptor", "I");
    mFileDescriptor = env->NewObject(cFileDescriptor, iFileDescriptor);
    env->SetIntField( mFileDescriptor, descriptorID, (jint)fd);
    env->ReleaseStringUTFChars(path_, path);
    return mFileDescriptor;
}

6、简单测试

public class MainActivity extends AppCompatActivity implements PortDataReceived {

    private static final String SERIAL_PATH = "/dev/ttySAC2";
    private static final int BAUD_RATE = 115200;
    EditText edit_content;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        edit_content = findViewById(R.id.edit_content);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        SerialPortManager.getInstance().unregist(this);
    }

    /**
     *  打开串口
     */
    public void open(View view) {
        SerialPortManager.getInstance().openSerialPort(SERIAL_PATH, BAUD_RATE);
        SerialPortManager.getInstance().regist(this);
    }

    /**
     * 发送数据
     */
    public void send(View view) {
        String command = edit_content.getText().toString().trim();
        if (TextUtils.isEmpty(command)) {
            return;
        }
        byte[] sendContentBytes = command.getBytes();
        SerialPortManager.getInstance().putCommand(sendContentBytes);
    }

    @Override
    public void onDataReceived(byte[] bytes) {
        Log.e("cmo", "   收到串口数据---->" + new String(bytes));
    }
}

猜你喜欢

转载自blog.csdn.net/CrazyMo_/article/details/104309983