准备工作
- NDK环境
- 串口通信使用的C文件
注意:c文件中需要修改函数名为当前包名的路径,c文件和.h头文件都需要修改
接入底层库
使用SO库引入
- main目录下新建jni录,将C文件置于该处
- 新建Android.mk、Application.mk文件并配置
- 编译项目,得到编译好的so库文件,生成目录默认为:app\build\intermediates\jniLibs\app\debug\armeabi,如果编译的是release版本则在releasse目录下
- main目录下新建目录jniLibs,将生成的so库拷贝到此
- 配置app目录下的gradle文件
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
ndk {
moduleName "serial_port"
ldLibs "log", "z", "m"
abiFilters "armeabi", "x86"
}
}
debug {
ndk {
moduleName "serial_port"
ldLibs "log", "z", "m"
abiFilters "armeabi", "x86"
}
}
}
- 代码中创建Class文件,加载这个so库文件,并定义打开、关闭串口的方法
CMake方式引入
- main目录下新建cpp文件夹,将c源文件置于此
- 配置CMakeLists.txt文件
cmake_minimum_required(VERSION 3.4.1)
# 查找cpp目录下的所有源文件
# 并将名称保存到 DIR_LIB_SRC 变量
aux_source_directory(src/main/cpp/ DIR_LIB_SRC)
#设置源文件
add_library(SerialPort SHARED ${DIR_LIB_SRC})
#表明头文件位置
#include_directories(src/main/cpp/)
find_library( # Sets the name of the path variable.
log-lib
log)
target_link_libraries( # Specifies the target library.
SerialPort
${log-lib})
- 配置app下gradle文件
android {
defaultConfig {
...
externalNativeBuild {
cmake {
cppFlags ""
}
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}
- 代码中创建Class文件,加载这个so库文件,并定义打开、关闭串口的方法
打开/关闭串口
jni中的打开关闭
C文件中定义了两个方法,分别为打开、关闭串口。打开串口方法具有返回值,返回值为FileDescriptor对象,我们通过这个对象实现对串口的数据读取和写入操作
打开串口
查询串口
一些定制的设备可能会有很多串口,我们应用究竟需要打开哪一个串口呢?一方面,可以询问设备厂商我们需要连接的串口名,另一方面就只能列出所有串口然后一个一个试了
查询所有串口:
定义SerialPortFinder类,通过读取/proc/tty/drivers文件,列举出设备的所有串口和路径
方法:SerialPortFinder中的getAllDevices()方法,具体实现见代码SerialPortFinder
打开
找到相应的串口后,调用jni方法打开串口,打开时需要传入三个参数,分别是串口路径,波特率,打开方式
public SerialPort getSerialPort() throws SecurityException, IOException, InvalidParameterException {
if (mSerialPort == null) {
//这里是打开了/dev/ttyS3 串口,波特率9600,
mSerialPort = new SerialPort(new File("/dev/ttyS3"), 9600, 0);
}
return mSerialPort;
}
通过打开串口,得到FileDescriptor对象,根据该对象创建一个输入输出流,从而实现读写操作。
关闭串口
使用完毕后、退出应用前,需要关闭串口。相当于文件操作一样需要关闭文件流,串口通信实质上也是文件操作,所以需要关闭
/**
* 关闭串口
* 使用结束时记得关闭串口
*/
public void closeSerialPort() {
if (mSerialPort != null) {
//调用native方法关闭串口
mSerialPort.close();
mSerialPort = null;
}
}
发送/读取数据
打开串口时获取到了输入输出流,那么接下来的发送和读取就是对这个流进行操作了。
发送数据:
发送数据为byte数组,如果是字符串啊、十六进制指令啊等等,都需要转换成byte数组发送。向我们的输出流写数据即可。
public void sendBufferToSerial(byte[] data) {
try {
mFileOutputStream.write(data);
} catch (IOException e) {
e.printStackTrace();
}
}
读取数据:
读取数据相对于发送数据而言,相对步骤会多一点,因为我们不知道串口什么时候会给我发送数据过来,所以需要开启一个子线程不断地从输入流取数据,当取到数据长度不为0时,对数据进行解析,从而进行下一步操作
class ReadThread extends Thread {
@Override
public void run() {
super.run();
while (!isInterrupted()) {
int size;
final byte[] buffer;
try {
buffer = new byte[128];
if (mFileInputStream == null) return;
size = mFileInputStream.read(buffer);
if (size > 0) {
//回调
if (listener != null)
listener.receiveData(size, buffer);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 开始接收串口数据
*
* @param onReceiveDataListener 数据回调
*/
public void startReceiveData(OnReceiveDataListener onReceiveDataListener) {
readThread = new ReadThread();
readThread.start();
listener = onReceiveDataListener;
}
/**
* 停止接收数据
*/
public void stopReceiveData() {
if (readThread != null) {
if (readThread.isInterrupted())
readThread.interrupt();
}
readThread = null;
listener = null;
}
读取到的同样是byte数组的数据,需要对数据进行解析,一般设备供应商会提供相应的解析方式。
串口的读取、发送数据,都会有2-5个字节作为识别位,一般都是0xFF开头,每个包都是统一的,发送和解析数据时需要注意
使用
可以选择在Application中打开串口,然后在项目的BaseActivity或者BaseFragment中定义好发送、读取数据的方法,需要使用的地方直接调用
附
C源文件: