📖 说明

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

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

🔍 Android串口通信原理

Android串口通信的核心原理:

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

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

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

参考资料:
LibModbus 源码参考

🔧 串口查看与准备

前置要求:

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

操作步骤

串口检查命令:

# 连接设备shell
adb shell

# 获取管理员权限
su

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

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

💻 代码实现

🔌 打开串口

核心函数:Java_com_crow_modbus_serialport_SerialPort_open

/*
 * 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

/*
 * 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);
}
  • 上层接口封装
  • 数据读写操作
  • 异常处理机制
  • 串口参数配置

🔌 接口声明

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()
}

🚩 串口管理封装

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. ✅ 了解文件描述符创建和管理

🔗 相关链接