利用CMake进行JNI串口操作
三方应用的操作串口方式
由于三方应用无法通过系统的串口API操作串口,需要借助JNI访问系统串口节点。
步骤如下:
新建Android Native LIbrary,生成CMakeLists.txt和SerialPort.cpp。
CMakeLists.txt如下:
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.18.1)
# Declares and names the project.
project("serialport")
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
add_library( # Sets the name of the library.
serialport
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
SerialPort.cpp)
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log)
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
target_link_libraries( # Specifies the target library.
serialport
# Links the target library to the log library
# included in the NDK.
${log-lib})
编写串口的c++代码,操作系统API的方式,SerialPort.cpp代码如下:
#include <string>
#include <termios.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cstring>
#include <jni.h>
#include "android/log.h"
static const char *TAG = "serial_port";
jmethodID FindMethod(JNIEnv *env, jclass klass, const char *name, const char *signature);
jfieldID FindField(JNIEnv *pEnv, jclass klass, const char *name, const char *desc);
#define LOGD(fmt, args...) __android_log_print(ANDROID_LOG_DEBUG, TAG, fmt, ##args)
#define LOGE(fmt, args...) __android_log_print(ANDROID_LOG_ERROR, TAG, fmt, ##args)
static speed_t getBandRate(jint bandRate) {
switch (bandRate) {
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 jint JNICALL Java_com_example_serialport_SerialPort_open
(JNIEnv *env, jobject thiz, jstring path, jint baudrate, jint flags) {
int fd;
speed_t speed;
jobject fileDescriptor;
/* Check arguments */
{
speed = getBandRate(baudrate);
if (speed == -1) {
return -1;
}
}
/* Opening device */
{
jboolean is_copy;
const char *path_utf = (*env).GetStringUTFChars(path, &is_copy);
LOGD("Opening serial port %s with flags 0x%x", path_utf, O_RDWR | flags);
fd = open(path_utf, O_RDWR | flags);
LOGD("open() fd = %d", fd);
(*env).ReleaseStringUTFChars(path, path_utf);
if (fd == -1) {
/* Throw an exception */
LOGE("Cannot open port");
return -1;
}
}
/* Configure device */
{
struct termios cfg{};
LOGD("Configuring serial port");
if (tcgetattr(fd, &cfg)) {
close(fd);
return -1;
}
cfmakeraw(&cfg);
cfsetispeed(&cfg, speed);
cfsetospeed(&cfg, speed);
if (tcsetattr(fd, TCSANOW, &cfg)) {
close(fd);
return -1;
}
}
/* Create a corresponding file descriptor */
{
jclass fileDescriptorClass = (*env).FindClass("java/io/FileDescriptor");
jmethodID fileDescriptorInitMethod = FindMethod(env, fileDescriptorClass, "<init>", "()V");
jfieldID fileDescriptorDescriptorField = FindField(env, fileDescriptorClass, "descriptor",
"I");
fileDescriptor = (*env).NewObject(fileDescriptorClass, fileDescriptorInitMethod);
(*env).SetIntField(fileDescriptor, fileDescriptorDescriptorField, (jint) fd);
}
return fd;
}
jmethodID FindMethod(JNIEnv *env, jclass klass, const char *name, const char *signature) {
jmethodID result = env->GetMethodID(klass, name, signature);
if (result == nullptr) {
abort();
}
return result;
}
jfieldID FindField(JNIEnv *env, jclass klass, const char *name, const char *desc) {
jfieldID result = env->GetFieldID(klass, name, desc);
if (result == nullptr) {
abort();
}
return result;
}
extern "C" JNIEXPORT void JNICALL Java_com_example_serialport_SerialPort_close
(JNIEnv *env, jobject thiz, jint fd) {
close(fd);
}
extern "C" JNIEXPORT jint JNICALL Java_com_example_serialport_SerialPort_readArray
(JNIEnv *env, jobject thiz, jint fd, jbyteArray buffer, jint length) {
auto *buf = (jbyte *) malloc(length);
if (!buf) {
LOGE("jniThrowException java/lang/OutOfMemoryError");
return -1;
}
int ret = read(fd, buf, length);
if (ret > 0) {
// copy data from native buffer to Java buffer
env->SetByteArrayRegion(buffer, 0, ret, buf);
}
free(buf);
if (ret < 0)
LOGE("jniThrowException java/io/IOException");
return ret;
}
extern "C" JNIEXPORT jint JNICALL Java_com_example_serialport_SerialPort_readDirect
(JNIEnv *env, jobject thiz, jint fd, jobject buffer, jint length) {
auto *buf = (jbyte *) env->GetDirectBufferAddress(buffer);
if (!buf) {
LOGE("ByteBuffer not direct: jniThrowException java/lang/IllegalArgumentException");
return -1;
}
int ret = read(fd, buf, length);
if (ret < 0)
LOGE("jniThrowException java/io/IOException");
return ret;
}
extern "C" JNIEXPORT void JNICALL Java_com_example_serialport_SerialPort_writeArray
(JNIEnv *env, jobject thiz, jint fd, jbyteArray byteArray, jint length) {
auto *buf = (jbyte *) malloc(length);
if (!buf) {
LOGE("jniThrowException java/lang/OutOfMemoryError");
}
env->GetByteArrayRegion(byteArray, 0, length, buf);
jint ret = write(fd, buf, length);
free(buf);
if (ret < 0)
LOGE("jniThrowException java/io/IOException");
}
extern "C" JNIEXPORT void JNICALL Java_com_example_serialport_SerialPort_writeDirect
(JNIEnv *env, jobject thiz, jint fd, jobject buffer, jint length) {
auto *buf = (jbyte *) env->GetDirectBufferAddress(buffer);
if (!buf) {
LOGE("ByteBuffer not direct: jniThrowException java/lang/IllegalArgumentException");
return;
}
int ret = write(fd, buf, length);
if (ret < 0)
LOGE("jniThrowException java/io/IOException");
}
在java依赖c++库,这里用的是JNI静态注册System.loadLibrary
,定义Native方法,与c++代码中对应,SerialPort.java代码如下:
package com.example.serialport;
import java.io.IOException;
import java.nio.ByteBuffer;
public class SerialPort {
static {
System.loadLibrary("serialport");
}
private int fd = -1;
public void openSerialPort(String path, int bandRate, int flags) {
fd = open(path, bandRate, flags);
}
public void closeSerialPort() {
close(fd);
}
public int read(ByteBuffer buffer) throws IOException {
if (fd < 0) {
return -1;
}
if (buffer.isDirect()) {
return readDirect(fd, buffer, buffer.remaining());
} else if (buffer.hasArray()) {
return readArray(fd, buffer.array(), buffer.remaining());
} else {
throw new IllegalArgumentException("buffer is not direct and has no array");
}
}
public void write(ByteBuffer buffer, int length) throws IOException {
if (fd < 0) {
return;
}
if (buffer.isDirect()) {
writeDirect(fd, buffer, length);
} else {
if (!buffer.hasArray()) {
throw new IllegalArgumentException("buffer is not direct and has no array");
}
writeArray(fd, buffer.array(), length);
}
}
/**
* A native method that is implemented by the 'serialport' native library,
* which is packaged with this application.
*/
private native int open(String path, int bandRate, int flags);
private native void close(int fd);
private native int readArray(int fd, byte[] buffer, int flags) throws IOException;
private native int readDirect(int fd, ByteBuffer buffer, int flags) throws IOException;
private native void writeArray(int fd, byte[] buffer, int flags) throws IOException;
private native void writeDirect(int fd, ByteBuffer buffer, int flags) throws IOException;
}
接下来对SerialPort的封装,SerialPortManager继承Thread,同时需要检查串口节点的读写权限。代码如下:
package com.example.serialport;
import android.util.Log;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
public class SerialPortManager extends Thread {
private static final String TAG = SerialPortManager.class.getSimpleName();
private static final int DATA_BUFFER_SIZE = 512; //最大字节长度
private final SerialPort mSerialPort;
private final ByteBuffer mInputBuffer;
private final ByteBuffer mOutputBuffer;
private SerialPortDataListener mSerialPortDataListener;
public SerialPortManager() {
mSerialPort = new SerialPort();
mInputBuffer = ByteBuffer.allocate(DATA_BUFFER_SIZE);
mOutputBuffer = ByteBuffer.allocate(DATA_BUFFER_SIZE);
}
public void openSerialPort(String name, int speed) {
if (name != null && mSerialPort != null && checkAccessPermission(name)) {
mSerialPort.openSerialPort(name, speed, 0);
}
}
/* Check access permission */
private boolean checkAccessPermission(String name) {
File device = new File(name);
if (!device.canRead() || !device.canWrite()) {
try {
/* Missing read/write permission, trying to chmod the file */
Process su;
su = Runtime.getRuntime().exec("/system/bin/su");
String cmd = "chmod 666 " + device.getAbsolutePath() + "\n"
+ "exit\n";
su.getOutputStream().write(cmd.getBytes());
if ((su.waitFor() != 0) || !device.canRead()
|| !device.canWrite()) {
throw new SecurityException();
}
} catch (Exception e) {
e.printStackTrace();
throw new SecurityException();
}
}
return true;
}
public void closeSerialPort() {
if (mSerialPort != null) {
mSerialPort.closeSerialPort();
}
}
@Override
public void run() {
int ret = 0;
byte[] buffer;
while (ret >= 0 && mSerialPort != null) {
try {
mInputBuffer.clear();
ret = mSerialPort.read(mInputBuffer);
} catch (IOException e) {
e.printStackTrace();
break;
}
if (ret > 0 && mSerialPortDataListener != null) {
buffer = new byte[ret];
mInputBuffer.get(buffer, 0, ret);
mSerialPortDataListener.onDataRead(buffer);
}
}
}
public void writeData(byte[] data) {
if (mSerialPort != null && data.length > 0 && data.length <= DATA_BUFFER_SIZE) {
try {
mOutputBuffer.clear();
mOutputBuffer.put(data);
mSerialPort.write(mOutputBuffer, data.length);
} catch (IOException e) {
e.printStackTrace();
}
} else {
Log.e(TAG, "writeDataToRemote failed");
}
}
public void setSerialPortDataListener(SerialPortDataListener listener) {
if (listener != null) {
this.start();
mSerialPortDataListener = listener;
}
}
public interface SerialPortDataListener {
void onDataRead(byte[] data);
}
}
可通过DATA_BUFFER_SIZE配置最大支持的缓冲字节长度。通过setSerialPortDataListener
设置监听串口读到的数据。
完成以上步骤后就可以通过SerialPortManager读写串口了。
系统应用操作串口的方式
系统应用可以访问到framework隐藏的API,可通过串口相关的API进行串口操作。主要有android.hardware.SerialManager
和android.hardware.SerialPort。
SerialManager serialManager = (SerialManager)context.getSystemService(Context.SERIAL_SERVICE);
SerialPort serialPort = serialManager.openSerialPort(name, speed);
操作方式而上述三方应用的方式类似。
相关源码地址:
/frameworks/base/core/java/android/hardware/SerialPort.java
/frameworks/base/core/jni/android_hardware_SerialPort.cpp
/frameworks/base/core/java/android/hardware/SerialManager.java
/frameworks/base/services/core/java/com/android/server/SerialService.java
/frameworks/base/services/core/jni/com_android_server_SerialService.cpp
/libnativehelper/JNIHelp.cpp
串口设备
1.串口分为Tx和Rx,Tx代表发送,Rx代表接收。相对客户端所处的设备连接的另一个串口设备而言,客户端的Tx对应远端设备的Rx,Rx对应远端设备的Tx,这样两边设备形成收发回路来进行通信。
2.串口设备间连接需要考虑共地的问题,否则会造成一端接收异常等现象。
3.不同硬件设备所支持的串口读写字节的最大长度不一样,需要考虑是否出现断帧的现象。
4.串口设备节点一般以/dev/tty***格式,例如/dev/ttyMT2、/devttyS0。
5.串口波特率BandRate需要两端设备相匹配,否则会出现乱码,所支持的波特率见上述getBandRate
方法。