关于Android串口通讯总结

前言

这几天完了串口,暂时还没搞懂这是啥玩意,因为目前底层到底如何通讯的我还不知道,不过这里先总结一下这两天的收获。

正文

现在我们开始我们最主要的问题,因为串口作为底层实现,linux把设备作为文件,并且串口文件在dev目录下的,并且现在都是通过c代码来打开的(貌似Java无法设置波特率啥的,这个东西c代码我暂时也搞不懂。并且我们cat这个文件的时候是得不到文件。以后我有机会研究研究,说不定可以实现呢。)。
这里我们关注两个问题。一个是路径,因为我们Android貌似好几个串口。所以你一定要知道你链接的串口的路径。第二波特率,这个是传输频率。非常重要,不然会出现乱码。一般如果我们可以收到消息,不能正常工作,大部分都是波特率不对。

对于不清楚如何使用jni的。我这里我推荐两个东西,Android Studio很强大,已经支持直接编译c/c++代码了,所以呢(Android studio 2.2+版本)。这里我就不再介绍之前用Javah的方法来编译jni的代码。具体教程在Android Studio 引入C/C++, 这里我还是不再详细介绍,后续我会给大家一个详细教程。

首先我们对于我们关注的串口我们会有两个操作,打开和关闭。

#include <jni.h>
#include <string>
#include <termios.h>
#include <unistd.h>
#include <fcntl.h>
#include <android/log.h>
//这里有没有简单的方法我也不知道啊。为啥这么怪的实现方法呢。大家可以给我指点。我只能这样搞了!!!
#define TAG "serial Port" // 这个是自定义的LOG的标识
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG ,__VA_ARGS__) // 定义LOGD类型
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG ,__VA_ARGS__) // 定义LOGI类型
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,TAG ,__VA_ARGS__) // 定义LOGW类型
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG ,__VA_ARGS__) // 定义LOGE类型
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,TAG ,__VA_ARGS__) // 定义LOGF类型
static speed_t getBaudRate(jint baudRate) {
    switch (baudRate) {
        case 0: return B0;
        case 50: return B50;
        case 75: return B75;
        case 110: return B110;
        case 134: return B134;
        case 150: return B150;
        case 200: return B200;
        case 300: return B300;
        case 600: return B600;
        case 1200: return B1200;
        case 1800: return B1800;
        case 2400: return B2400;
        case 4800: return B4800;
        case 9600: return B9600;
        case 19200: return B19200;
        case 38400: return B38400;
        case 57600: return B57600;
        case 115200: return B115200;
        case 230400: return B230400;
        case 460800: return B460800;
        case 500000: return B500000;
        case 576000: return B576000;
        case 921600: return B921600;
        case 1000000: return B1000000;
        case 1152000: return B1152000;
        case 1500000: return B1500000;
        case 2000000: return B2000000;
        case 2500000: return B2500000;
        case 3000000: return B3000000;
        case 3500000: return B3500000;
        case 4000000: return B4000000;
        default:
            return -1;
    }
}

extern "C"
JNIEXPORT jobject JNICALL Java_zzxcomm_keylock_Util_SerialPort_open
        (JNIEnv *env, jobject obj, jstring devicePath, jint buaRate, jint flags) {
    int fd;
    speed_t speed;
    jobject mFd;
    speed   = getBaudRate(buaRate);
    if (buaRate == 115200){
        LOGE("Invalid buaR");
    }
    if (speed == -1) {
        LOGE("Invalid buaRate!!");
        return NULL;
    }
    LOGI("Right buaRate = %d.", buaRate);
    /** open device */
    jboolean isCopy;
    const char *utfPath = env->GetStringUTFChars(devicePath, &isCopy);
    fd  = open(utfPath, O_RDWR | flags);
    env->ReleaseStringUTFChars(devicePath, utfPath);
    if (fd == -1) {
        LOGE("Cannot open port");
    }
    LOGI("Open port Success!");
    /** Configure Device*/
    struct termios cfg;
    /** 获取与该终端描述符有关的参数,结果保存在termios结构体中.成功返回0
    c_iflag:输入模式标志,控制终端输入方式.
    c_oflag:输出模式标志.
    c_cflag:控制模式标志,指定终端硬件控制信息.
    c_lflag:本地模式标志,控制终端编辑功能.
    c_cc[NCCS]:控制字符,用于保存终端驱动程序中的特殊字符,如输入结束符.
    **/
    if (tcgetattr(fd, &cfg)) {
        LOGE("tcgetattr() failed");
        close(fd);
        return NULL;
    }
    LOGI("tcgetattr() Success");
    /** 设置终端属性为原始属性 **/
    cfmakeraw(&cfg);
    /** 设置输入波特率 */
    cfsetispeed(&cfg, speed);
    /** 设置输出波特率 */
    cfsetospeed(&cfg, speed);
    /** 设置属性
    第二个参数表示什么时候生效.
    TCSANOW:表明该设置立即生效
    TCSADRAIN:在所有写入fd的输出都输出后生效.此参数该在参数影响输出时使用
    TCSAFLUSH:清空输入输出缓冲区才改变属性.所有写入 fd 引用的对象的输出都被传输后生效,所有已接受但未读入的输入都在改变发生前丢弃.
    **/
    if (tcsetattr(fd, TCSANOW, &cfg)) {
        LOGE("tcsetattr() failed");
        close(fd);
        return NULL;
    }
    LOGI("tcsetattr() Success");
    /** Create a corresponding file descriptor */
    jclass cFileDescriptor  = env->FindClass("java/io/FileDescriptor");
    jmethodID iFileDescriptor   = env->GetMethodID(cFileDescriptor, "<init>", "()V");
    jfieldID descriptorID   = env->GetFieldID(cFileDescriptor, "descriptor", "I");
    mFd = env->NewObject(cFileDescriptor, iFileDescriptor);
    env->SetIntField(mFd, descriptorID, (jint) fd);
    LOGI("return mFd = %d.", fd);
    return mFd;
}

/*
 * Class:     com_zzx_port_SerialPort
 * Method:    close
 * Signature: ()V
 */
extern "C"
JNIEXPORT void JNICALL Java_zzxcomm_keylock_Util_SerialPort_close
        (JNIEnv *env, jobject obj) {
    jclass SerialPortClass = env->GetObjectClass(obj);
    jclass FileDescriptorClass = env->FindClass("java/io/FileDescriptor");
    jfieldID descriptorID = env->GetFieldID(FileDescriptorClass, "descriptor", "I");

    jfieldID mFDID = env->GetFieldID(SerialPortClass, "mFd", "Ljava/io/FileDescriptor;");
    jobject mFd = env->GetObjectField(obj, mFDID);

    jint descriptor = env->GetIntField(mFd, descriptorID);
    close(descriptor);
}

这里的东西我也不太懂,总是就是获取了一个文件的操作指针,也是句柄。总之就是你获取了文件的操作方法。。然后我们看下Java代码

import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * Created by Administrator on 2016/6/17.
 * 串口通信
 */
public class SerialPort {

    private FileInputStream mInput;
    private FileOutputStream mOutput;
    private FileDescriptor mFd;

    private native FileDescriptor open(String path, int baudRate, int flags);

    public native void close();

    static {
        System.loadLibrary("native-lib");
    }

    /**
     * 构造函数
     * @param portPath 串口路径.
     * @param baudRate 串口波特率.
     * @param flags    串口类型.一般为0.
     */
    public SerialPort(String portPath, int baudRate, int flags) throws SecurityException, IOException {
        File file = new File(portPath);
        if (!file.canRead() || !file.canWrite()) {
        //这里很重要,在Android 5.0 之后,这里无法获取root 权限,所以无法读取到我们串口的消息。所以呢,只能找底层人帮忙了。
            try {
                Process su;
                su = Runtime.getRuntime().exec("/system/xbin/su");
                String cmd = "chmod 666 " + portPath + "\n";
                su.getOutputStream().write(cmd.getBytes());
                if ((su.waitFor() != 0) || !file.canWrite() || !file.canRead()) {
                    throw new SecurityException();
                }
                su.destroy();
            } catch (Exception e) {
                e.printStackTrace();
                throw new SecurityException();
            }
        }
        try {
            mFd = open(portPath, baudRate, flags);
            if (mFd == null) {
                throw new IOException();
            }
        } catch (Exception e) {
            e.printStackTrace();
            LogUtil.loge("serial port","is failed");
            return;
        }
        mInput = new FileInputStream(mFd);
        mOutput = new FileOutputStream(mFd);
        LogUtil.loge("serial port","is open");
    }

    /**
     * 获取输入流
     */
    public InputStream getInputStream() {
        return mInput;
    }

    /**
     * 获取输出流
     */
    public OutputStream getOutputStream() {
        return mOutput;
    }

    public void doClose(){
        close();
    }
}

这里打开文件。然后获取到io流,以便于在应用中读取,其实这里我们已经获取到我们该得到的东西了,不过我们还是注意一下,因为我们需要不停的读取这个串口,近似于监听效果,并且我们这里使用一个单例模式,以便于获取与不至于是程序混乱。代码如下

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * 串口操作类
 *
 * @author Jerome
 *
 */
public class SerialPortUtil {
    private SerialPort mSerialPort;
    private OutputStream mOutputStream;
    private InputStream mInputStream;
    private ReadThread mReadThread;
    private String path = "/dev/ttyMT0";
    private int baudrate = 115200;
    private static SerialPortUtil portUtil;
    private OnDataReceiveListener onDataReceiveListener = null;
    private boolean isStop = false;

    public interface OnDataReceiveListener {
        void onSerialDataReceive(byte[] buffer, int size);
    }

    public void setOnDataReceiveListener(
            OnDataReceiveListener dataReceiveListener) {
        onDataReceiveListener = dataReceiveListener;
    }

    public static SerialPortUtil getInstance() {
        if (null == portUtil) {
            portUtil = new SerialPortUtil();
            portUtil.onCreate();
        }
        return portUtil;
    }

    /**
     * 初始化串口信息
     */
    public void onCreate() {
        try {
            mSerialPort = new SerialPort(path, baudrate,0);
            mOutputStream = mSerialPort.getOutputStream();
            mInputStream = mSerialPort.getInputStream();

            mReadThread = new ReadThread();
            isStop = false;
            mReadThread.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private boolean sendData(byte[] data){
        boolean result = true;
        try {
            if (mOutputStream != null) {
                mOutputStream.write(data);
                LogUtil.logd("发出",data);
            } else {
                result = false;
            }
        } catch (IOException e) {
            e.printStackTrace();
            result = false;
        }
        return result;
    }

    public void sendBuffer(byte[] mBuffer) {
        if (!sendData(mBuffer)){
            closeSerialPort();
            onCreate();
            sendBuffer(mBuffer);
        }
    }

    private class ReadThread extends Thread {

        @Override
        public void run() {
            super.run();
            while (!isStop && !isInterrupted()) {
                int size;
                try {
                    if (mInputStream == null)
                        return;
                    byte[] buffer = new byte[512];
                    size = mInputStream.read(buffer);
                    if (size > 0) {
                        if (null != onDataReceiveListener) {
                            onDataReceiveListener.onSerialDataReceive(buffer, size);
                        }
                    }
                    Thread.sleep(10);
                } catch (Exception e) {
                    e.printStackTrace();
                    return;
                }
            }
        }
    }

    /**
     * 关闭串口
     */
    public void closeSerialPort() {
        isStop = true;
        if (mReadThread != null) {
            mReadThread.interrupt();
        }
        if (mSerialPort != null) {
            try {
                mSerialPort.doClose();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }

}

这里我们就已经获取了串口的读取与发送的代码控制了。基本算是整个个流程都完成了。不在详细介绍,对于接收数据后如何处理。这里本人也没找到好的办法,如果只在一个界面前面代码保留了一个监听接口,基本满足我们需求,但是如果要分发数据的话,这里就不太容易处理了,
这里提供几个思路

  1. eventbus、或者广播。这东西确实对于解耦很有用,缺点:黑洞效应比较明显,对于简单应用,有点复杂。
  2. 直接设置一个OnDataReceiveListener,用来分发消息。这里逻辑更加简单,可是遇到更加复杂的问题,很难解决

    这里具体如何选择我就不再详细介绍。对于选择困难综合症患者。建议eventbus。

注意事项

我们通过数组得到的一般是inputstreame,然后很容易转换成byte数组,这里我们很容易知道每个byte包含8个字节,可以存储256个字符,但是这些字符有些无法表示,所以呢在很多通讯协议中都是讲一个byte转换成两个十六进制的数字表示,这里我知道的大概有四五种解析方法,我这里不一一解释,这篇博客暂时只给出一个方法。以后我有空再给大家补充

final static String HEX = "0123456789ABCDEF";
private String getHexString(byte[] buffer) {
    StringBuilder sb = new StringBuilder(buffer.length * 2);
    for (int i=0;i<buffer.length;i++) {
    sb.append(HEX.charAt((buffer[i] >> 4) & 0x0f));
            //取出字节的低4位,然后与 0x0f与运算,得到0~15的数据,通过HEX.charAt(0~15),即为十六进制数.
        sb.append(HEX.charAt(buffer[i] & 0x0f));
    }
    LogUntils.logv(this,sb.toString());
    return sb.toString();
}

具体我就不解释了,这里逻辑比较简单。小面我们稍微说点小技巧。其实很简单,但是我总是忘记,


因为byte是8位,理论是上无符号,但是假如最高位为1的时候,再向int型转换的时候,会变成有符号的数据,这里我们记住,byte是个八byt的二进制数值,强制转换成int的时候只取这个八位的数值,所以会出现负数,当我们使用int型时,在八byt的范围内永远不会有负值。
而int型向byte转换仅仅取第八位的数值,因为byte不关心正负号,他仅仅是一种编码符号。所以加入我们需要判断获取的一个byte和int型。一般是可以

byt buffer = a;
int code =  97;
if((byte)code  == buffer){
}

或者:

byte buffer = a;
int code =  97;
if(code  == (byt)buffer&0xff){
}

这里byte如果简单的算法还是要知道Java的数据存储方式比较好。具体自己理解。


Android 5.0之后包括5.0,权限貌似有问题。具体如何读取串口。我暂时没找到好的方法


还有几个问题。关于消息的处理。当读取的消息不是一条,也就是在那个时间段内,读取的东西不是一条消息指令。我们需要截取指令长度,然后处理。这里可以结合自己code来处理

如果中间发错指令或者,或者需要重新发送。这里需要建立一个消息队列,这里的问题比较复杂,不在详细介绍,这种情况一般很少发生,基本可以不用考虑。

后记

总之这个算是写完了,遇到一些问题,希望大家指正.具体代码
一个版本的源码:这里写代码片

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值