📖 说明

本篇文章将详细介绍如何通过JNI在Android中实现串口通信

  • 在上一篇文章中我们配置了JNI的基础环境和相关上层、C层函数声明
  • 接下来将编写相关代码来打开串口并进行通信

🔍 Android串口通信原理

Android串口通信的核心原理:

  1. Root权限要求:设备需要root权限才能访问串口文件
  2. 文件句柄操作:通过文件句柄来管理串口的相关信息(位置、权限、IO信息等)
  3. 串口配置:获取文件句柄后可以对串口进行配置、交互等操作

重要提示:文件句柄是用于管理文件相关信息的抽象概念,包括文件位置、权限、IO信息等

代码核心部分都添加了详细注释,完整源码请查看:
KModbus - GitHub

参考资料:
LibModbus 源码参考

🔧 串口查看与准备

前置要求:

  • 确保设备已Root
  • 准备ADB工具并配置环境变量

操作步骤

串口检查命令:

1
2
3
4
5
6
7
8
9
10
11
# 连接设备shell
adb shell

# 获取管理员权限
su

# 显示串口列表
ls /dev | grep ttyS

# 查看某个串口是否占用(以ttyS0为例)
lsof /dev/ttyS0

💻 代码实现

🔌 打开串口

核心函数:Java_com_crow_modbus_serialport_SerialPort_open

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
/*
* Class: android_serialport_SerialPort
* Method: open
* Signature: (Ljava/lang/String;II)Ljava/io/FileDescriptor;
*
* Description:
* Opens a serial port and configures its parameters such as baud rate, data bits, stop bits, and parity.
*
* Parameters:
* path - The path to the serial port device (e.g., "/dev/ttyS0").
* baudrate - The desired baud rate (e.g., 9600).
* flags - Flags for opening the serial port (e.g., O_RDWR).
* parity - Parity mode (0 for none, 1 for even, 2 for odd).
* stop_bit - Stop bit mode (1 or 2).
* data_bit - Data bits (5, 6, 7, or 8).
*
* Returns:
* A FileDescriptor object representing the opened serial port, or NULL if an error occurs.
* @author : revise by crowforkotlin
* @link : https://github.com/stephane/libmodbus/blob/b25629bfb508bdce7d519884c0fa9810b7d98d44/src/modbus-rtu.c#L661
*/
JNIEXPORT jobject JNICALL Java_com_crow_modbus_serialport_SerialPort_open
(JNIEnv *env, jclass thiz, jstring path, jint baudrate, jint flags, jint parity, jint stop_bit, jint data_bit) {
LOGD("----------------------------------------------------------");
int fd;
speed_t speed;
jobject mFileDescriptor;

// 校验波特率是否正确
{
speed = getBaudrate(baudrate);
if (speed == -1) {
LOGE("Invalid baudrate!");
return NULL;
}
}

// 开启串口 O_RDWR 开启读写权限
{
jboolean iscopy;
const char *path_utf = (*env)->GetStringUTFChars(env, path, &iscopy);
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(env, path, path_utf);
if (fd == -1) {
LOGE("Cannot open port");
return NULL;
}
}

// 配置串口
{
struct termios cfg;
LOGD("Configuring serial port");
// 尝试获取文件描述符fd标识并存储到cfg结构体中
if (tcgetattr(fd, &cfg)) {
LOGE("tcgetattr() failed");
close(fd);
return NULL;
}

// 设置原始模式,禁用自动特殊字符处理、奇偶校验等功能
// 以便更精确地控制串口数据的传输
cfmakeraw(&cfg);

// 设置输入输出波特率
cfsetispeed(&cfg, speed);
cfsetospeed(&cfg, speed);

/* 设置数据位 (5, 6, 7, 8 bits)
* CSIZE Bit mask for data bits
*/
cfg.c_cflag &= ~CSIZE;
switch (data_bit) {
case 5:
cfg.c_cflag |= CS5;
break;
case 6:
cfg.c_cflag |= CS6;
break;
case 7:
cfg.c_cflag |= CS7;
break;
case 8:
default:
cfg.c_cflag |= CS8;
break;
}

/* 设置停止位 (1 or 2) */
if (stop_bit == 1)
cfg.c_cflag &= ~CSTOPB;
else /* 2 */
cfg.c_cflag |= CSTOPB;

// 设置校验位模式 Enable parity bit
if (parity == 0) {
LOGD("NONE");
cfg.c_cflag &= ~PARENB;
} else if (parity == 1) {
LOGD("Even");
cfg.c_cflag |= PARENB;
cfg.c_cflag &= ~PARODD;
} else {
LOGD("ODD");
cfg.c_cflag |= PARENB;
cfg.c_cflag |= PARODD;
}

if (tcsetattr(fd, TCSANOW, &cfg)) {
LOGE("tcsetattr() failed");
close(fd);
return NULL;
}
}

// 创建文件描述符
{
// 获取类、构造函数ID、字段ID 最后实例化FileDescriptor后并设置字段
jclass cFileDescriptor = (*env)->FindClass(env, "java/io/FileDescriptor");
jmethodID iFileDescriptor = (*env)->GetMethodID(env, cFileDescriptor, "<init>", "()V");
jfieldID descriptorID = (*env)->GetFieldID(env, cFileDescriptor, "descriptor", "I");
mFileDescriptor = (*env)->NewObject(env, cFileDescriptor, iFileDescriptor);
(*env)->SetIntField(env, mFileDescriptor, descriptorID, (jint) fd);
}

return mFileDescriptor;
}

🔒 关闭串口

核心函数:Java_com_crow_modbus_serialport_SerialPort_close

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
* Class: cedric_serial_SerialPort
* Method: close
* Signature: ()V
*
* Description:
* Closes the serial port by releasing the file descriptor associated with it.
*/
JNIEXPORT void JNICALL Java_com_crow_modbus_serialport_SerialPort_close(JNIEnv *env, jobject thiz) {
jclass SerialPortClass = (*env)->GetObjectClass(env, thiz);
jclass FileDescriptorClass = (*env)->FindClass(env, "java/io/FileDescriptor");

jfieldID mFdID = (*env)->GetFieldID(env, SerialPortClass, "mFileDescriptor", "Ljava/io/FileDescriptor;");
jfieldID descriptorID = (*env)->GetFieldID(env, FileDescriptorClass, "fd", "I");

jobject mFd = (*env)->GetObjectField(env, thiz, mFdID);
jint descriptor = (*env)->GetIntField(env, mFd, descriptorID);

LOGD("close(fd = %d)", descriptor);
close(descriptor);
}
  • 上层接口封装
  • 数据读写操作
  • 异常处理机制
  • 串口参数配置

🔌 接口声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
open class SerialPort protected constructor() {

var mFileDescriptor: FileDescriptor? = null

companion object {
init {
// 加载共享库
System.loadLibrary("SerialPort")
}
}

/**
* ● 打开串口
* @param path 串口文件路径
* @param baudrate 波特率
* @param parity 校验
* @param stopbit 停止位 1 或 2
* @param databit 数据位 5 - 8
* ● 2023-09-25 18:29:58 周一 下午
*/
protected fun open(path: String, baudrate: Int, parity: SerialPortParityFunction, stopbit: Int, databit: Int): FileDescriptor {
if (stopbit !in 1..2) {
throw IllegalStateException("stopbit must in 1..2!")
}
if (databit !in 5..8) {
throw IllegalStateException("databit must in 5..8!")
}
return open(path, baudrate, 0, parity.code, stopbit, databit)
}

// 定义JNI方法,用于打开串口
private external fun open(path: String, baudrate: Int, flags: Int, parity: Int, stopbits: Int, databits: Int): FileDescriptor

// 定义JNI方法,用于关闭串口
protected external fun close()
}

🚩 串口管理封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
class SerialPortManager : SerialPort() {

companion object {
private val mutex = Mutex()
private val parentJob = SupervisorJob()
private val io = CoroutineScope(Dispatchers.IO + parentJob + CoroutineExceptionHandler { coroutineContext, throwable ->
loggerError("SerialPort an Exception occurs! context is $coroutineContext \t exception : ${throwable.stackTraceToString()}")
})
}

private var mSuccessListener: IOpenSerialPortSuccess? = null
private var mFailureListener: IOpenSerialPortFailure? = null

private var mFileInputStream: FileInputStream? = null
private var mFileOutputStream: FileOutputStream? = null

private val mReadedBuffer = Bytes(1024)

/**
* ● 修改文件权限为可读、可写、可执行
*
* ● 2023-09-23 11:41:26 周六 上午
*/
private fun changeFilePermissions(file: File): Boolean {
return (file.takeIf { it.exists() } ?: false).runCatching {

logger("info")

// 获取ROOT权限
val su = Runtime.getRuntime().exec("/system/bin/su")

logger(su)

// 修改文件属性为 [可读 可写 可执行]
val cmd = "chmod 777 ${file.absolutePath}\nexit\n"

// 将命令写入 su 进程的输出流
su.outputStream.write(cmd.toByteArray())

// 如果 su 进程返回值为 0 并且文件可读、可写、可执行,则返回 true
(su.waitFor() == 0 && file.canRead() && file.canWrite() && file.canExecute())
}
.onFailure { catch ->
when (catch) {
is IOException -> logger("No root permission!")
else -> logger(catch.stackTraceToString())
}
}
.getOrElse { false }
}

/**
* ● 打开串口
*
* ● 2023-09-23 16:02:30 周六 下午
*/
fun openSerialPort(path: String, baudRate: Int) {

val device = File(path)

// 校验串口权限
if (!device.canRead() || !device.canWrite()) {
if (!changeFilePermissions(device)) {
loggerError("openSerialPort : 没有读写权限!")
mFailureListener?.onFailure(device, SerialPortState.NO_READ_WRITE_PERMISSION)
return
}
}

mFileDescriptor = open(device.absolutePath, baudRate, SerialPortParityFunction.EVEN, 1, 8)
mFileInputStream = FileInputStream(mFileDescriptor)
mFileOutputStream = FileOutputStream(mFileDescriptor)
mSuccessListener?.onSuccess(device)
logger("openSerialPort : 串口已经打开 $mFileDescriptor")
}

/**
* ● 关闭串口
*
* ● 2023-09-23 16:02:12 周六 下午
*/
fun closeSerialPort(): Boolean {
return runCatching {
parentJob.children.forEach { job -> job.cancel() }
mFileDescriptor = null
mFileInputStream?.close()
mFileOutputStream?.close()
mFileInputStream = null
mFileOutputStream = null
mSuccessListener = null
mFailureListener = null
true
}
.onFailure { catch -> loggerError("close serial port exception! ${catch.stackTraceToString()}") }
.getOrElse { false }
}

@OptIn(ExperimentalStdlibApi::class)
fun writeBytes(bytes: ByteArray) {
io.launch {
mutex.withLock {
if (null != mFileDescriptor && null != mFileInputStream && null != mFileOutputStream) {
logger("writeBytes ${bytes.map { it.toHexString() }}")
mFileOutputStream!!.write(bytes)
}
}
}
}

fun readBytes(iDataReceive: IDataReceive) {
io.launch {
while (true) {
if (null != mFileDescriptor && null != mFileInputStream && null != mFileOutputStream) {
val length = mFileInputStream!!.read(mReadedBuffer)
logger("$length")
if (length <= 0) return@launch
val buffer = Bytes(length)
System.arraycopy(mReadedBuffer, 0, buffer, 0, length)
iDataReceive.onReceive(buffer)
}
}
}
}
}

🎯 关键要点总结

本文重点内容:

  1. ✅ 理解Android串口通信的基本原理
  2. ✅ 掌握JNI层串口开启和关闭的实现
  3. ✅ 学会串口参数的配置方法
  4. ✅ 了解文件描述符创建和管理

🔗 相关链接