# CmakeGoogleSerialPort **Repository Path**: AndroidCoderPeng/CmakeGoogleSerialPort ## Basic Information - **Project Name**: CmakeGoogleSerialPort - **Description**: cmake方式编译Google串口通信 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-03-15 - **Last Updated**: 2025-05-11 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # CmakeGoogleSerialPort Android NDK开发入门,零基础,以Google官方串口通信作为例子,编译方式为cmake ---- ## JNI和NDK介绍 参看我另一个开源项目[GoogleSerialPort](https://gitee.com/AndroidCoderPeng/GoogleSerialPort) (ndk-build方式编译) ### 1、新建项目 因为采用cmake方式编译,所以新建项目方式和[GoogleSerialPort](https://gitee.com/AndroidCoderPeng/GoogleSerialPort) 略有不同,这里需要新建一个C++项目,如下图: ![](app/screenshot/1.png) 然后在选择编译工具链的时候默认就行,不必修改,如下图: ![](app/screenshot/2.png) 至于新项目在建完之后会自动触发Gradle任务,如果因Gradle报错而导致项目起不来的,先自行搜索解决方案,此问题不是不是本项目介绍重点。 如果Gradle编译之后出现如下错误 ![](app/screenshot/3.png) 是因为没有配置NDK导致,在[local.properties](local.properties)一下就好了,如下图: ![](app/screenshot/4.png) 建好之后如下图: ![](app/screenshot/5.png) NDK编译之前就说过有两种方式。一种是ndk-build,另一种是cmake,咱先用ndk-build编译过,这次就用交叉编译方式——cmake编译。 ### 2、在Android项目中声明Native方法,如下所示: ```kotlin class SerialPort(device: File, baudRate: Int, flags: Int) { private val kTag = "SerialPort" init { System.loadLibrary("serial_port") } /** * 打开串口 */ private external fun open(path: String, baudRate: Int, flags: Int): FileDescriptor? /** * 关闭串口 */ private external fun close() } ``` 这一步和ndk-build一样,声明Java/Kotlin与C/C++的桥接函数,和ndk-build不同的是,此时不需要Build项目。 ### 3、完善SerialPort逻辑,完整代码如下: ```kotlin class SerialPort(device: File, baudRate: Int, flags: Int) { private val kTag = "SerialPort" private var fd: FileDescriptor? = null var inputStream: InputStream var outputStream: OutputStream init { System.loadLibrary("serial_port") if (!device.canRead() || !device.canWrite()) { try { val su = Runtime.getRuntime().exec("/system/bin/su") val cmd = "chmod 666 ${device.absolutePath} \n exit \n" su.outputStream.write(cmd.toByteArray()) if (su.waitFor() != 0 || !device.canRead() || !device.canWrite()) { throw SecurityException() } } catch (e: Exception) { e.printStackTrace() throw SecurityException() } } fd = open(device.absolutePath, baudRate, flags) if (fd == null) { Log.e(kTag, "SerialPort open return null") throw IOException() } inputStream = FileInputStream(fd) outputStream = FileOutputStream(fd) } fun closeSerialPort() { close() } private external fun open(path: String, baudRate: Int, flags: Int): FileDescriptor? private external fun close() } ``` 在爆红的native标识的方法出按option+回车键(Mac)或者Ctrl+回车键(Windows),然后就会自动在[serial_port.cpp](app/src/main/cpp/serial_port.cpp) 生成JNI实现函数,如下图: ![](app/screenshot/6.png) 有几个注意事项: a、每个函数前面的 ```objectivec extern "C" JNIEXPORT void JNICALL ``` 一定不能删除,因为串口底层相关逻辑是C语言编写,而[serial_port.cpp](app/src/main/cpp/serial_port.cpp) 是C++代码,C++调用C函数,就必须加上 ```objectivec extern "C" JNIEXPORT void JNICALL ``` b、生成的函数名不能修改,如果非要改,那就直接改[SerialPort.kt](app/src/main/java/com/pengxh/cmake/serialport/uart/SerialPort.kt) 里面的native方法名,然后重新生成一次。 c、之前C语言写的串口打开关闭函数与cmake条件下C++写的实现函数略有不同,看不明白的直接复制即可:[serial_port.cpp](app/src/main/cpp/serial_port.cpp)。 ### 4、说一下[CMakeLists.txt](app/src/main/cpp/CMakeLists.txt) 默认情况下是不需要修改此配置里面的内容的,但是如果修改过C++的文件名,那么也需要将此文件里面的 ```cmake project("serial_port") ``` 引号里面改为和你修改的C++文件名保持一致,否则编译失败。同样还需要和[SerialPort.kt](app/src/main/java/com/pengxh/cmake/serialport/uart/SerialPort.kt) 里面的代码保持一致 ```kotlin class SerialPort(device: File, baudRate: Int, flags: Int) { init { System.loadLibrary("serial_port") } } ``` 另外:Gradle里面的externalNativeBuild配置是新建项目时自动生成的,是CMakeLists.txt的路径,一般别动,如果要改,租要注意CMakeLists.txt的新路径要正确 ```Gradle { externalNativeBuild { cmake { path file('src/main/cpp/CMakeLists.txt') version '3.22.1' } } } ``` 经过以上4个步骤,项目文件结构应该是这样的: ![](app/screenshot/7.png) ## 经过对比发现,用cmake编译也没有生成 .so 库,那怎么调用呢? 其实并不是没有生成,是有的,在目录[app/build/intermediates/cmake/debug/obj/](app/build/intermediates/cmake/debug/obj/arm64-v8a/libserialPort.so) 下,但是通过cmake编译的项目不需要手动引用 .so 库,直接编译既可通过Java/Kotlin调用C++代码 还是以串口通信为例,这个对于Android开发来说较简单也较实用 1、BaseApplication:主要是打开/关闭串口等初始化操作 ```kotlin class BaseApplication : Application() { private val kTag = "BaseApplication" private var serialPorts = ArrayList() fun getSerialPorts(): ArrayList = serialPorts fun closeSerialPort() { serialPorts.forEach { it.closeSerialPort() } } companion object { private var application: BaseApplication by Delegates.notNull() fun get() = application } override fun onCreate() { super.onCreate() application = this SaveKeyValues.initSharedPreferences(this) /** * Open the serial port * */ try { serialPorts.apply { add(SerialPort(File("/dev/ttysWK1"), 9600, 0)) } Log.d(kTag, "onCreate: 已初始化 ${serialPorts.size} 个串口") } catch (e: SecurityException) { "您没有串口的读写权限!".show(this) } catch (e: IOException) { "因为不明原因,串口无法打开!".show(this) } catch (e: InvalidParameterException) { "请检查串口!".show(this) } } } ``` 2、[SerialPortBaseActivity](app/src/main/java/com/pengxh/cmake/serialport/base/SerialPortBaseActivity.kt)是个抽象类,集中处理串口通信 ```kotlin abstract class SerialPortBaseActivity : AppCompatActivity() { private val kTag = "SerialPortBaseActivity" private val serialPorts by lazy { BaseApplication.get().getSerialPorts() } protected lateinit var binding: VB override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = initViewBinding() setContentView(binding.root) setupTopBarLayout() initOnCreate(savedInstanceState) observeRequestState() initEvent() if (serialPorts.isNotEmpty()) { lifecycleScope.launch(Dispatchers.IO) { val buffer = ByteArray(128) try { while (isActive) { val bytesRead = serialPorts.first().inputStream.read(buffer) if (bytesRead > 0) { withContext(Dispatchers.Main) { onDataReceived(buffer) } } } } catch (e: IOException) { e.printStackTrace() } } } } /** * 初始化ViewBinding */ abstract fun initViewBinding(): VB /** * 特定页面定制沉浸式状态栏 */ abstract fun setupTopBarLayout() /** * 初始化默认数据 */ abstract fun initOnCreate(savedInstanceState: Bundle?) /** * 数据请求状态监听 */ abstract fun observeRequestState() /** * 初始化业务逻辑 */ abstract fun initEvent() /** * 串口读数 * */ abstract fun onDataReceived(buffer: ByteArray) fun sendSerialPortCommand(command: Char) { if (serialPorts.isNotEmpty()) { val outStream = serialPorts.first().outputStream outStream.write(command.code) outStream.flush() Log.d(kTag, "sendSerialPortCommand: $command") } } } ``` 后续3/4/5步骤见[GoogleSerialPort](https://gitee.com/AndroidCoderPeng/GoogleSerialPort)的README.md ---- 以上就是通过cmake编译方式的串口通信。