阅读 183

搞懂Android串口通信(如何明白Android串口通信?)

1、串口通信是什么

串行通信技术,是指通信双方按位进行,遵守时序的一种通信方式
说人话就是将数据按位依次传输
画个图

image.png 串口就相当于一个管道,在硬件方面也有表示,有三根跳线, 一个是Tx线,一个是Rx线,还有一根是地线,这个管道传输的数据,也就是bit是串行的,有顺序的

2、串口的应用场景

串口通信这个东西,在Android开发中用到的并不多,我们绝大多数App都是用Http和后台进行通信,获取后台数据并展示,而串口通信是应用在,智能家居,和单片机通信的场景,人脸识别门禁,利用串口控制门开关,自动售货机Android收到付款成功的消息后,发送串口指令,控制货道进行出货等等 Android的设备已经超过20亿了,相对来说串口在Android应用还是挺广泛的

3、Android怎么实现串口通信的

3.1、第一步找到串口文件

Android的串口文件是有一个单独的目录的

企业微信截图_16352252408824.png

image.png

image.png 我们操作的就是这个ttys开头的文件
用代码是怎么操作的呢

private ArrayList<Driver> getDrivers() throws IOException {     ArrayList<Driver> drivers = new ArrayList<>();     LineNumberReader lineNumberReader = new LineNumberReader(new FileReader(DRIVERS_PATH));     String readLine;     while ((readLine = lineNumberReader.readLine()) != null) {         String driverName = readLine.substring(0, 0x15).trim();         String[] fields = readLine.split(" +");         // driverName:/dev/tty         // driverName:/dev/console         // driverName:/dev/ptmx         // driverName:/dev/vc/0         // driverName:serial         // driverName:pty_slave         // driverName:pty_master         // driverName:unknown         Log.d(T.TAG, "SerialPortFinder getDrivers() driverName:" + driverName /*+ " readLine:" + readLine*/);         if ((fields.length >= 5) && (fields[fields.length - 1].equals(SERIAL_FIELD))) { // 判断第四个等不等于serial             // 找到了新串口驱动是:serial 此串口系列名是:/dev/ttyS             Log.d(T.TAG, "SerialPortFinder getDrivers() 找到了新串口驱动是:" + driverName + " 此串口系列名是:" + fields[fields.length - 4]);             drivers.add(new Driver(driverName, fields[fields.length - 4]));         }     }     return drivers; } 复制代码

3.2、第二步打开串口文件

我们操作串口的时候我们首先要检验一下权限

if (!device.canRead() || !device.canWrite()) {     boolean chmod777 = chmod777(device);     if (!chmod777) {         Log.i(T.TAG, "SerialPortManager openSerialPort: 没有读写权限");         if (null != mOnOpenSerialPortListener) {             mOnOpenSerialPortListener.onFail(device, OnOpenSerialPortListener.Status.NO_READ_WRITE_PERMISSION);         }         return false;     } } /**  * 文件设置最高权限 777 可读 可写 可执行  * @param  file 你要对那个文件,获取root权限  * @return 权限修改是否成功- 返回:成功 与 失败 结果  */ boolean chmod777(File file) {     if (null == file || !file.exists()) {         // 文件不存在         return false;     }     try {         // 获取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; } 复制代码

检验完权限之后,我们就要用ndk的代码去打开串口进行操作,然后java层和 Native层的联系就是文件句柄FileDescriptor也就是代码中的fd,Native层返回FileDescriptor,然后Java层的FileInputStream、FileOutputStream和FileDescriptor进行绑定,这样Java层就能读取到数据

try {     mFd = openNative(device.getAbsolutePath(), baudRate, 0); // 打开串口-native函数     mFileInputStream = new FileInputStream(mFd); // 读取的流 绑定了 (mFd文件句柄)-通过文件句柄(mFd)包装出 输入流     mFileOutputStream = new FileOutputStream(mFd); // 写入的流 绑定了 (mFd文件句柄)-通过文件句柄(mFd)包装出 输出流     Log.i(T.TAG, "SerialPortManager openSerialPort: 串口已经打开 " + mFd); // 串口已经打开 FileDescriptor[35] 【2】     if (null != mOnOpenSerialPortListener) {         mOnOpenSerialPortListener.onSuccess(device);     }     startSendThread(); // 开启发送消息的线程     startReadThread(); // 开启接收消息的线程     return true; // 【3】 } catch (Exception e) {     e.printStackTrace();     if (null != mOnOpenSerialPortListener) {         mOnOpenSerialPortListener.onFail(device, OnOpenSerialPortListener.Status.OPEN_FAIL);     } } 复制代码

Native

JNIEXPORT jobject JNICALL Java_com_test_openNative   (JNIEnv *env, jclass thiz, jstring path, jint baudrate, jint flags) {    int fd; // Linux串口文件句柄(本次整个函数最终的关键成果)    speed_t speed; // 波特率类型的值    jobject mFileDescriptor; // 文件句柄(最终返回的成果)    //检查参数,获取波特率参数信息 [先确定好波特率]    {       speed = getBaudrate(baudrate);       if (speed == -1) {          LOGE("无效的波特率,证明用户选择的波特率 是错误的");          return NULL;       }    }    // TODO 第一步:打开串口    {       jboolean iscopy;       const char *path_utf = (*env)->GetStringUTFChars(env, path, &iscopy);       LOGD("打开串口 路径是:%s", path_utf); // 打开串口 路径是:/dev/ttyS0       fd = open(path_utf, O_RDWR /*| flags*/); // 打开串口的函数,O_RDWR(读 和 写)       LOGD("打开串口 open() fd = %d", fd); // open() fd = 44       (*env)->ReleaseStringUTFChars(env, path, path_utf); // 释放操作       if (fd == -1) {          LOGE("无法打开端口");          return NULL;       }    }    LOGD("第一步:打开串口,成功了√√√");    // TODO 第二步:获取和设置终端属性-配置串口设备    /* TCSANOW:不等数据传输完毕就立即改变属性。        TCSADRAIN:等待所有数据传输结束才改变属性。        TCSAFLUSH:清空输入输出缓冲区才改变属性。        注意:当进行多重修改时,应当在这个函数之后再次调用 tcgetattr() 来检测是否所有修改都成功实现。      */    {       struct termios cfg;       LOGD("执行配置串口中...");       if (tcgetattr(fd, &cfg)) { // 获取串口属性          LOGE("配置串口tcgetattr() 失败");          close(fd); // 关闭串口          return NULL;       }       cfmakeraw(&cfg);          // 将串口设置成原始模式,并且让fd(文件句柄 对串口可读可写)       cfsetispeed(&cfg, speed); // 设置串口读取波特率       cfsetospeed(&cfg, speed); // 设置串口写入波特率       if (tcsetattr(fd, TCSANOW, &cfg)) { // 根据上面的配置,再次获取串口属性          LOGE("再配置串口tcgetattr() 失败");          close(fd); // 关闭串口          return NULL;       }    }    LOGD("第二步:获取和设置终端属性-配置串口设备,成功了√√√");    // TODO 第三步:构建FileDescriptor.java对象,并赋予丰富串口相关的值    {       jclass cFileDescriptor = (*env)->FindClass(env, "java/io/FileDescriptor");       jmethodID iFileDescriptor = (*env)->GetMethodID(env, cFileDescriptor, "<init>", "()V");       jfieldID descriptorID = (*env)->GetFieldID(env, cFileDescriptor, "descriptor", "I");       // 反射生成FileDescriptor对象,并赋值 (fd==Linux串口文件句柄) FileDescriptor的构造函数实例化       mFileDescriptor = (*env)->NewObject(env, cFileDescriptor, iFileDescriptor);       (*env)->SetIntField(env, mFileDescriptor, descriptorID, (jint)fd); // 这里的fd,就是打开串口的关键成果    }    LOGD("第三步:构建FileDescriptor.java对象,并赋予丰富串口相关的值,成功了√√√");    return mFileDescriptor; // 把最终的成果,返回会Java层 } 复制代码

这样我们就完成了整个打开串口的操作

image.png

3.3、发送和读取数据

我们读取和发送数据是对文件IO进行操作,我们肯定要在子线程中进行,

private void startReadThread() {     mSerialPortReadThread = new SerialPortReadThread(mFileInputStream) {         @Override         public void onDataReceived(byte[] bytes) {             if (null != mOnSerialPortDataListener) {                 mOnSerialPortDataListener.onDataReceived(bytes);             }         }     };     mSerialPortReadThread.start(); } /**  * 串口消息读取线程  *       开启接收消息的线程  *       读取 串口数据 需要用到线程  */ public abstract class SerialPortReadThread extends Thread {     public abstract void onDataReceived(byte[] bytes);     private static final String TAG = SerialPortReadThread.class.getSimpleName();     private InputStream mInputStream; // 此输入流==mFileInputStream(关联mFd文件句柄)     private byte[] mReadBuffer; // 用于装载读取到的串口数据     public SerialPortReadThread(InputStream inputStream) {         mInputStream = inputStream;         mReadBuffer = new byte[1024]; // 缓冲区     }     @Override     public void run() {         super.run();         // 相当于是一直执行?为什么要一直执行?因为只要App存活,就需要读取 底层发过来的串口数据         while (!isInterrupted()) {             try {                 if (null == mInputStream) {                     return;                 }                 Log.i(TAG, "run: ");                 int size = mInputStream.read(mReadBuffer);                 if (-1 == size || 0 >= size) {                     return;                 }                 byte[] readBytes = new byte[size];                 // 拷贝到缓冲区                 System.arraycopy(mReadBuffer, 0, readBytes, 0, size);                 Log.i(TAG, "run: readBytes = " + new String(readBytes));                 onDataReceived(readBytes); // 发出去-(间接的发到SerialPortActivity中去打印显示)             } catch (IOException e) {                 e.printStackTrace();                 return;             }         }     }     @Override     public synchronized void start() {         super.start();     }     /**      * 关闭线程 释放资源      */     public void release() {         interrupt();         if (null != mInputStream) {             try {                 mInputStream.close();                 mInputStream = null;             } catch (IOException e) {                 e.printStackTrace();             }         }     } } private void startSendThread() {     // 开启发送消息的线程     mSendingHandlerThread = new HandlerThread("mSendingHandlerThread");     mSendingHandlerThread.start();     // Handler     mSendingHandler = new Handler(mSendingHandlerThread.getLooper()) {         @Override         public void handleMessage(Message msg) {             byte[] sendBytes = (byte[]) msg.obj;             if (null != mFileOutputStream && null != sendBytes && 0 < sendBytes.length) {                 try {                     mFileOutputStream.write(sendBytes);                     if (null != mOnSerialPortDataListener) {                         mOnSerialPortDataListener.onDataSent(sendBytes); // 【发送 1】                     }                 } catch (IOException e) {                     e.printStackTrace();                 }             }         }     }; } 复制代码

读取和写入数据,其实就是对那两个读入,读处流进行操作,就这样我们就完成了对串口的收发数据

3.4关闭串口

我们用完串口之后,肯定会把串口关闭的,关闭串口,我们就把启动的读和写的线程关掉,在Native层也把串口关掉,将文件句柄绑定的两个流也关掉

/**  * 关闭串口  */ public void closeSerialPort() {     if (null != mFd) {         closeNative(); // 关闭串口-native函数         mFd = null;     }     stopSendThread(); // 停止发送消息的线程     stopReadThread(); // 停止接收消息的线程     if (null != mFileInputStream) {         try {             mFileInputStream.close();         } catch (IOException e) {             e.printStackTrace();         }         mFileInputStream = null;     }     if (null != mFileOutputStream) {         try {             mFileOutputStream.close();         } catch (IOException e) {             e.printStackTrace();         }         mFileOutputStream = null;     }     mOnOpenSerialPortListener = null;     mOnSerialPortDataListener = null; } 复制代码

Native层

/*  * 关闭串口  * Class:     cedric_serial_SerialPort  * Method:    close  * Signature: ()V  */ JNIEXPORT void JNICALL Java_com_test_closeNative   (JNIEnv *env, jobject thiz) {    jclass SerialPortClass = (*env)->GetObjectClass(env, thiz);    jclass FileDescriptorClass = (*env)->FindClass(env, "java/io/FileDescriptor");    jfieldID mFdID = (*env)->GetFieldID(env, SerialPortClass, "mFd", "Ljava/io/FileDescriptor;");    jfieldID descriptorID = (*env)->GetFieldID(env, FileDescriptorClass, "descriptor", "I");    jobject mFd = (*env)->GetObjectField(env, thiz, mFdID);    jint descriptor = (*env)->GetIntField(env, mFd, descriptorID);    LOGD("关闭串口 close(fd = %d)", descriptor);    close(descriptor); // 把此串口文件句柄关闭掉-文件读写流(文件句柄) InputStream/OutputStream=串口 发/收 } 复制代码

4、总结

串口通信,其实就是对文件进行操作,一边读一边写,就跟上学时你和同桌传纸条似得,以上代码参考的是谷歌的开源的代码,从寻找串口到关闭串口,梳理了一下串口通信的基本流程!希望对XDM有用,希望兄弟们一键三连!


作者:被遗忘的凉白开
链接:https://juejin.cn/post/7023271438782038052


文章分类
后端
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐