基本常识
串口通信:指串口按位(bit)发送和接收字节。尽管比按字节(byte)的并行通信慢,但是串口可以使用一根线发送数据的同时接收数据。在串口通信中,常用的协议包括RS232、RS-422和RS-485。
在Android开发中,对串口数据的读取和写入,实际上是是通过I/O流读取、写入文件数据。
串口用完记得关闭(文件关闭)。 串口关闭,即是文件流关闭。
一、准备so库以及相关SDK
用到开源库serialPort-api
下载其中相关so库资源导入到项目中
其中libs文件夹中的armeabi、armeabi-v7a代表不同的cpu架构,可以根据自己app运行环境自动加载,通常只需要armeabi就可以了。android_serialport_api包下的两个文件则是用来加载so库以及提供相关接口的封装SDK;
注意:这里的包名不可以修改,必须跟so库中的保持一致(这同样在以后对接其他外设SDK时,在加载so库方法经常会遇到方法名加载不到的原因)
在拷贝完相关so库之后我们仍然需要在gradle中指定我们支持的cpu架构,保持libs包中一致即可:
defaultConfig {
ndk {
abiFilters "armeabi","armeabi-v7a" // "armeabi", "x86", "arm64-v8a"
}
}
SerialPort类:
/*
* Copyright 2009 Cedric Priscal
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android_serialport_api;
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.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Scanner;
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, int parity) throws SecurityException, IOException {
/* Check access permission */
if (!device.canRead() || !device.canWrite()) {
try {
/* Missing read/write permission, trying to chmod the file */
//Runtime.getRuntime().exec 会提示需要ACCESS_SUPERUSER权限
Process su;
su = Runtime.getRuntime().exec("/system/bin/su");
// su = Runtime.getRuntime().exec("/system/xbin/su");
String cmd = "chmod 666 " + device.getAbsolutePath() + "\n" + "exit\n";
su.getOutputStream().write(cmd.getBytes());
WatchThread wt = new WatchThread(su);
wt.start();
if ((su.waitFor() != 0) || !device.canRead() || !device.canWrite()) {
ArrayList<String> commandStream = wt.getStream();
wt.setOver(true);
throw new SecurityException();
}
} catch (Exception e) {
e.printStackTrace();
throw new SecurityException();
}
}
mFd = open(device.getAbsolutePath(), baudrate, flags, parity);
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, int parity);
public native void close();
static {
System.loadLibrary("serial_port");
}
class WatchThread extends Thread {
Process p;
boolean over;
ArrayList<String> stream;
public WatchThread(Process p) {
this.p = p;
over = false;
stream = new ArrayList<String>();
}
public void run() {
try {
if(p == null)return;
Scanner br = new Scanner(p.getInputStream());
while (true) {
if (p==null || over) break;
while(br.hasNextLine()){
String tempStream = br.nextLine();
if(tempStream.trim()==null||tempStream.trim().equals(""))continue;
stream.add(tempStream);
}
}
} catch(Exception e){e.printStackTrace();}
}
public void setOver(boolean over) {
this.over = over;
}
public ArrayList<String> getStream() {
return stream;
}
}
}
从这个类中我们可以看到,通过System.loadLibrary来加载我们对应的so库,“serial_port”名称则对应libs文件夹下面的armeabi包中的so库名称,如果不是一致的则会抛出异常提示加载so库失败。
static {
System.loadLibrary("serial_port");
}
当然我们有些也可以将so库放置在jni文件夹或者jnilib文件夹下面,如果我们so库防止的路径没有问题,但是在加载so库的时候仍然抛出异常提示找不到对应的so库,那么可能是我们程序并没有识别到相应的路径,这个时候我们就需要在gradle里面进行配置:
sourceSets.main {
jniLibs.srcDir "libs"
}
同时可以看到该类中封装的打开和关闭串口的方法:
private native static FileDescriptor open(String path, int baudrate, int flags, int parity);
public native void close();
如果没有权限的也需要配置相关的系统权限,包括我们进行串口初始化时需要的root权限也是必须要的,如果没有root权限则无法进行串口初始化。到此为止我们串口编程的基本准备工作已经全部准备就绪。
二、编写串口通讯工具类
首先我们需要编写的是串口通讯需要的工具类,该工具类主要解决一下几个问题:串口的打开、关闭、输入监听、输出等;同时该工具类作为全局唯一的操作类来进行串口通讯的统一管理:
package com.xiye.reservesamplecabinet.manager
import android_serialport_api.SerialPort
import com.xiye.reservesamplecabinet.utils.L
import com.xiye.reservesamplecabinet.utils.T
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.security.InvalidParameterException
/**
* Created by Administrator on 2018/8/3.
*/
class BoxActionManager private constructor() {
companion object {
val instance by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { BoxActionManager() }
private var mSerialPort: SerialPort? = null
private var mOutputStream: OutputStream? = null
private var mInputStream: InputStream? = null
private var mReadDataThread: Thread? = null
/**
* 初始化锁控串口
*/
@Throws(IOException::class)
private fun initSerialPort() {
if (mSerialPort == null) {
mSerialPort = getSerialPort(0, "dev/ttyS0", 9600)
if (mSerialPort == null) {
T.instance.showToast("串口初始化失败!")
return
}
mOutputStream = mSerialPort?.outputStream
mInputStream = mSerialPort?.inputStream
}
}
/**
* 初始化串口
*/
private fun getSerialPort(parity: Int, path: String, baudrate: Int): SerialPort? {
var mSerialPort: SerialPort? = null
if (path.isEmpty() || baudrate == -1) {
throw InvalidParameterException()
}
try {
mSerialPort = SerialPort(File(path), baudrate, 0, 0)
} catch (e: IOException) {
e.printStackTrace()
} catch (e: SecurityException) {
e.printStackTrace()
}
return mSerialPort
}
}
init {
if (mSerialPort == null) {
try {
initSerialPort()
} catch (e: IOException) {
e.printStackTrace()
}
}
initThread()
}
/**
* 初始化串口读取线程
*/
private fun initThread() {
if (mReadDataThread == null) {
mReadDataThread = Thread {
while (true) {
try {
if (mInputStream != null) {
val count = mInputStream!!.available()
val buffer = ByteArray(count)
val size = mInputStream!!.read(buffer)
val hexStr = TransUtils.bytes2hex(buffer)
if (size > 0) {
onDataReceiver(buffer)
}
try {
Thread.sleep(50L)
} catch (e: InterruptedException) {
e.printStackTrace()
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
mReadDataThread?.start()
}
}
private fun onDataReceiver(buffer: ByteArray) {
}
@Synchronized
fun openDoor(boardNo: Int, lockNo: Int) {
if (checkSerialPort()) {
return
}
val cmd = byteArrayOf(
boardNo.toByte(), 0xF2.toByte(), 0x55, lockNo.toByte()
)
try {
mOutputStream?.write(TransUtils.getSendDatas(cmd))
mOutputStream?.flush()
} catch (e: IOException) {
T.instance.showToast(e.message)
}
}
@Synchronized
fun queryDoor(boardNo: Int, lockNo: Int) {
if (checkSerialPort()) {
return
}
val cmd = byteArrayOf(
boardNo.toByte(), 0xF1.toByte(), 0x55, lockNo.toByte()
)
try {
mOutputStream?.write(TransUtils.getSendDatas(cmd))
mOutputStream?.flush()
} catch (e: IOException) {
T.instance.showToast(e.message)
}
}
}
在初始化方法中可以看到mSerialPort = getSerialPort(0, “dev/ttyS0”, 9600),是指定了一个固定的串口进行初始化,我们也可以通过SerialPortFinder类中提供的方法进行一个动态的搜索串口并进行相关初始化,需要根据项目不同的需求来灵活使用。
其中onDataReceiver方法则是监听到的输入信息,我们可以通过协议相关的约定进行一个解析来判断监听到的输入数据是否合法,同时解析出我们需要的数据帧。
同样我们是通过**mOutputStream?.write()**方法来对数据输出,输出的数据帧同样是根据协议中的约定来进行组帧。
感兴趣的同学可以通过NDK开发中jni配置及调用GPIO了解如何自己一步步编译属于自己的so库文件并运用到项目中