LSerialPort 一个C++完结多线程办理读写串口Android库
- [项目地址] github.com/RedRackham-…)
该库选用C++完结异步线程读写串口。上层运用无需过多关心异步线程读写问题。
arm64-v8a、armeabi-v7a
- 下载AAR
- 下载demo.apk
接口说明
函数称号 | 说明 | 回来值 | 补白 |
LSerialPort.openSerialPort | 敞开串口 | 0:成功 1:失利 |
对串口读写前必须先运用该函数翻开串口, |
LSerialPort.sendMsg | 发送二进制数据 | 0:成功 1:失利 |
异步发送音讯给串口。底层运用queue+mutex保护完结一个线程安全音讯行列。上层可在恣意线程内调用该函数。 |
LSerialPort.setOnLSerialPortListener | 设置串口接纳监听器 | 0:成功 1:失利 |
当接纳到串口数据时会调用该回调 留意!!!该回调线程为底层读线程,不引荐在该回调线程做任何耗时操作,以免影响线程音讯读取。 |
LSerialPort.closeSerialPort | 封闭串口 | 0:成功 1:失利 |
中止底层线程并封闭串口,该函数会堵塞调用线程直到资源开释完毕。 |
LSerialPort.hasOpen | 检查串口是否翻开 | true:已翻开false:未翻开 |
Native接口说明
以下是jni函数,不引荐直接运用。
函数称号 | 说明 | 回来值 |
LSerialPort.native_openSerialPort | 敞开串口 | 0:成功 1:失利 |
LSerialPort.native_sendMsg | 发送二进制数据 | 0:成功 1:失利 |
LSerialPort.native_setLSerialPortDataListener | 设置串口接纳监听器 | 无 |
LSerialPort.native_closeSerialPort | 封闭串口 | 0:成功 1:失利 |
LSerialPort.native_hasOpen | 检查串口是否翻开 | true:成功 false:失利 |
引入AAR
-
下载 AAR包放入工程内libs目录下
-
在build.gradle中dependencies内增加引证声明
implementation fileTree(dir: 'libs', include: ['*.jar','*.aar'])
开始运用(Kotlin)参阅代码
翻开串口
//翻开串口/dev/ttysWK0,波特率为9600
val result = LSerialPort.openSerialPort(path = "/dev/ttysWK0",baudrate = BaudRate.B_9600)
//当然也能够分别定制数据位,校验位,中止位。
//默许数据位:8,校验位:无,中止位:1
val result = LSerialPort.openSerialPort(
path = "/dev/ttysWK0", //串口地址
baudrate = BaudRate.B_9600,//波特率
dataBits = DataBits.EIGHT,//数据位
parity = Parity.NONE,//校验位
stopBits = StopBits.ONE//中止位
)
//假如需求一次回来尽量多一些数据。能够设置checkIntervalWaitMills参数。
//该函数是线程循环检查串口是否有数据并告诉回传的等候时刻,设置等候时刻越长,数据回来量越大,当然数据回调的距离也会越久,酌情装备。
//默许等候时刻:0 单位:毫秒
val result = LSerialPort.openSerialPort(
path = "/dev/ttysWK0",
baudrate = BaudRate.B_9600,
checkIntervalWaitMills = 100//设置等候时刻100ms
)
发送一条数据
//翻开串口后发送数据
val msg = byteArrayOf(0xFF.toByte(),0x01.toByte(),0x02.toByte(),0x03.toByte(),0x04.toByte(),0x05.toByte(),0xFE.toByte())
val result = LSerialPort.sendMsg("/dev/ttysWK0",msg)
//能够在子线程内发送数据,发送线程以及行列由C++部分保护,无需关心线程同步问题
Thread{
val msg = byteArrayOf(0xFF.toByte(),0x01.toByte(),0x02.toByte(),0x03.toByte(),0xFE.toByte())
val result = LSerialPort.sendMsg("/dev/ttysWK0",msg)
}.start()
设置监听器
//翻开串口后设置监听器 回来数据为byteArray
//留意! 假如进行多次设置,每次会覆盖掉前一个监听器。
val result = LSerialPort.setOnLSerialPortListener("/dev/ttysWK0") { msg ->
Log.d("LSerialPort","接纳到数据长度:${msg.size}")
}
封闭串口
//调用时该函数会堵塞等候C++线程退出,需求一定耗时
val result = LSerialPort.closeSerialPort("/dev/ttysWK0")
判别串口是否现已翻开
//运用该函数判别串口是否现已翻开
val isOpened:Boolean = LSerialPort.hasOpen("/dev/ttysWK0")
Log.d("LSerialPort","串口/dev/ttysWK0是否已翻开:$isOpened}")
工程开发环境信息
NDK :23.1.7779620
C++ :17
Android Gradle Plugin :7.4.1
Gradle :7.5
Android Studio :Android Studio Electric Eel | 2022.1.1 Patch 1
编译工程生成AAR
-
导入工程装备后选择Android Studio 中的build -> Refresh Linked C++ Projects 等候Gradle build完结。
-
Gradle build完结后选择Rebuild Project 等候Gradle build完结。
-
完结后在LSerialPort/build/outputs/aar/目录下会看到LSerialPort-debug.aar文件
可能会遇到的问题
1. 调用函数是回来-1失利,我该怎样检查是什么问题
回来错误码-1时,logcat中会打印错误信息,能够经过Error等级寻觅”LSerialPortLog”要害字检查。
package:mine level:error LSerialPortLog
2. 新版Studio怎样下载某个版别的NDK or 新版的SDK Manager找不到想要的NDK版别
首先,翻开Studio的Tools -> SDK Manage
然后,翻开页面后先选择 SDK Tools选项
最终,勾选Show Package Details选项即可下载想要版别的NDK
3. 想要打包运转其他架构的包。如x86平台
翻开LSerialPort库的Modele Gradle,在NDK内填写需求的架构,如下图内的x86
然后删去app以及LSerialPort中的build目录
最终从头编译生成AAR即可
4. 想要打包release版别的AAR
翻开studio的Build Variants。app与LSerialPort都改为release后,创立release需求的签名文件并从头打包
5. 为什么有时候Refresh Linked C++ Projects以及Rebuild Project后没有AAR文件
先把app以及LSerialPort目录下的build删去,然后从头Refresh Linked C++ Projects以及Rebuild Project,假如仍是没有生成多rebuild几回
LSerialPort设计思路
首先,咱们这儿对串口设计三个线程使命,分别处理对串口进行读,写,以及检查数据并告诉唤醒读线程进行数据读取。这儿多一个检查告诉线程的意义在于咱们能自行定制读取数据机遇,
经过修正每次检查后的等候时刻,能够削减读取次数,一起增加一次读取的数据量。
其次,因为涉及到多串口,咱们还要完结串口间的通讯,以及完结安全封闭线程以及收回资源。防止内存泄漏问题。
最终,还需求一个办理者,对每个翻开的串口进行增删查改功用。
下面是每个功用具体完结的介绍。
1.写线程
完结异步发送,这儿选用音讯行列的机制。上层开放接口往行列发送音讯,线程轮循读取音讯行列获取发送的音讯往串口发送。这儿咱们运用C++中的std::queue来作为音讯行列,可是std::queue是线程不安全的行列,所以需求
一个锁来确保读写行列时的线程安全,这儿运用std::mutex以及std::lock_guard来创立互斥锁来完结。现在只差一个问题,该怎样做到发送音讯给行列后告诉写线程读取行列中的音讯,完结线程之间的通讯?。这儿用到另一个C++标准库内的std::condition_variable来解决,std::condition_variable能够堵塞一个或多个线程,直到其他线程修正同享变量或条件,并告诉有意修正变量的线程。这样就完美契合咱们的要求。
下面分别是提供上层发送音讯的接口函数doWork以及写线程函数readLoop。
doWork-上层发送音讯函数
void ReadWriteWorker::doWork(const std::vector<uint8_t> &msg) {
//创立互斥锁确保线程安全
std::lock_guard<std::mutex> lk(_mMsgMutex);
//推送音讯到音讯行列_mMsgQueue
_mMsgQueue.push(msg);
//告诉音讯音讯等候的线程(包含读写线程),检查履行条件
_mMsgCond.notify_all();
}
writeLoop-写线程循环函数
//写循环
void ReadWriteWorker::writeLoop() {
// 等候直至 main() 发送数据
std::unique_lock<std::mutex> lk(_mMsgMutex);
LOGE("start write loop");
while (!isInterrupted()) {
//这儿判别当时线程没有符号退出,一起音讯行列中有音讯,持续运转后面的代码。
_mMsgCond.wait(lk, [&] { return (isInterrupted() || !_mMsgQueue.empty()); });
if (isInterrupted()) {
break;
}
//从音讯行列中获取音讯
std::vector<uint8_t> msg = std::move(_mMsgQueue.front());
_serialPort->WriteBinary(msg);
//音讯出列
_mMsgQueue.pop();
}
LOGE("write loop is interrupted!");
}
2.读线程
有了前面写线程的经验,读线程也能够运用前面说到的std::mutex、std::lock_guard以及std::condition_variable来完结线程之间的通讯以及确保共有数据的线程安全。线程问题现在现已解决了,那咱们该怎样把读到的数据回来给上层呢?答案是JNI中JNIEnv的GetObjectClass以及GetMethodID获取java回调函数把数据回传给上层。一起,咱们也增加一个监听器锁_mListenerMutex,完结安全设置监听器。
readLoop-写线程循环函数
void ReadWriteWorker::readLoop() {
LOGE("start read loop");
// 等候直至收到发送数据
std::unique_lock<std::mutex> lk(_mMsgMutex);
//C++线程首次拜访JEnv函数需求先运用AttachCurrentThread把当时线程附加到JVM上下文才干运用JEnv的函数、
//Jni的native函数不需求是因为是Jvm线程调用函数,所以不需求附加,C++创立线程不数据Jvm,所以要进行关联附加才干运用Jni的函数
if (LSerialPortManager::jvm->AttachCurrentThread(&_env, nullptr) != JNI_OK) {
return THROW_EXCEPT("unable to get jvm instance");
}
while (!isInterrupted()) {
//这儿判别当时线程没有符号退出,一起有数据的情况下中止堵塞,持续运转后面的代码。
_mMsgCond.wait(lk, [&] { return (isInterrupted() || _dataAvailable.load()); });
if (isInterrupted()) {
break;
}
//读取二进制
std::vector<uint8_t> msg_vec;
//读取数据
_serialPort->ReadBinary(msg_vec);
//重置符号
_dataAvailable.store(false);
if (msg_vec.empty()) {
//没数据视为超时,进行下一轮循环
continue;
}
//设置监听器锁
std::lock_guard<std::mutex> llk(_mListenerMutex);
if (_prepListener != nullptr) {
//假如之前现已设置过了,先收回删去上一个listener
if (_curlistener != nullptr) {
LOGE("there is currently a listener and it is being recycled...");
_curlistener->recycle(_env);
delete _curlistener;
}
_curlistener = _prepListener;
_prepListener = nullptr;
}
if (_curlistener != nullptr) {
_curlistener->onDataReceived(_env, msg_vec);
}
}
if (_prepListener != nullptr) {
_prepListener->recycle(_env);
}
if (_curlistener != nullptr) {
_curlistener->recycle(_env);
}
//退出当时线程
LSerialPortManager::jvm->DetachCurrentThread();
LOGE("read loop is interrupted!");
}
onDataReceived-回调读取到的数据给java
void LSerialPortDataListener::onDataReceived(JNIEnv *env, const std::vector<uint8_t> &msg) {
if (_onDataReceivedMethod == nullptr) {
jclass listenerClass = env->GetObjectClass(_jListener);
_onDataReceivedMethod = env->GetMethodID(listenerClass, "onDataReceived", "([B)V");
env->DeleteLocalRef(listenerClass);
}
auto size = static_cast<uint32_t>(msg.size());
jbyteArray jData;
if (size > static_cast<uint32_t>(std::numeric_limits<jsize>::max())) {
LOGE("data size is too big start splite send data!");
// 数据过大,需求分段复制
constexpr uint32_t chunkSize = std::numeric_limits<jsize>::max();
jData = env->NewByteArray(chunkSize);
uint32_t offset = 0;
while (offset < size) {
const uint32_t copySize = std::min(size - offset, chunkSize);
env->SetByteArrayRegion(jData, 0, copySize,
reinterpret_cast<const jbyte *>(msg.data() + offset));
offset += copySize;
env->CallVoidMethod(_jListener, _onDataReceivedMethod, jData);
env->DeleteLocalRef(jData);
}
} else {
// 直接复制到jbyteArray中
if (env == nullptr) {
LOGE("JNIEnv pointer is null!");
return;
}
jData = env->NewByteArray(static_cast<jsize>(size));
env->SetByteArrayRegion(jData, 0, static_cast<jsize>(size),
reinterpret_cast<const jbyte *>(msg.data()));
env->CallVoidMethod(_jListener, _onDataReceivedMethod, jData);
env->DeleteLocalRef(jData);
}
}
3.检查告诉线程
在设计思路中咱们说到过,增加一个检查告诉线程的目的是为了能自行定制读取数据的实践。满意一些需求削减读取次数一起时效性要求不怎样高,一次回来尽可能多数据的场景。这儿咱们经过获取到std::condition_variable对读线程进行通讯。
checkAvailableLoop-检查告诉数据接纳循环函数
void ReadWriteWorker::checkAvailableLoop() {
LOGE("start check available Loop");
while (!isInterrupted()) {
//判别串口目标状况是已翻开,一起串口有数据
if (_serialPort->GetState() == State::OPEN && _serialPort->Available() > 0) {
//修正读取数据符号
_dataAvailable.store(true);
//告诉音讯
_mMsgCond.notify_all();
}
//这儿设置毫秒等候时刻 等候x毫秒后再进行下一次检查
std::this_thread::sleep_for(std::chrono::milliseconds(_checkIntervalWaitMills));
}
LOGE("check available Loop is interrupted!");
}
- 到此咱们多线程对串口读写的功用现已完结,这儿封装成一个名叫ReadWriteWorker 的类来对功用进行办理。
4.安全封闭/退出
已然翻开了这么多线程处理使命,当然怎样安全的封闭线程以及串口也是咱们需求考虑的功用。这儿咱们首先是需求考虑该怎样封闭时告诉到各个线程退出循环完毕使命。在前面解说各个线程设计时讲到,线程运用std::condition_variable进行唤醒后会依据同享内容条件判别是否持续堵塞,在这儿咱们也能够运用这个特性增加判别机制。比如用std::atomic包装一个变量做原子操作进行条件判别。当然,LSerialPort里边咱们运用另外一种方式,那就是std::promise以及std::future来完结。std::promise提供一种存储值或反常的机制,而std::future是能拜访异步操作结果。std::promise能够创立std::future作关联运用。简单来说就是std::promise答应设置一个值,或者反常,这个值或反常能够在未来的某个时刻被关联的std::future目标拜访到。多线程中也能够经过std::future来完结异步参数获取。
这儿咱们把安全退出的判别抽出来作为一个抽象类IWorker,便利以后拓展。
IWorker.h
#ifndef LSERIALPORT_IWORKER_H
#define LSERIALPORT_IWORKER_H
#include "future"
/**
* 工作类接口
* 经过promise和future完结线程安全退出
*/
namespace LSerialPort {
class IWorker {
public:
/**
* 构造函数一起创立future
*/
IWorker() : _interruptedFuture(_interruptSignal.get_future()) {
}
~IWorker() {
}
//发送音讯
virtual void doWork(const std::vector<uint8_t> &msg) = 0;
//退出
virtual void interrupte() {
_interruptSignal.set_value();
}
//是否退出
bool isInterrupted() {
//设置等候时刻,假如当时状况非超时,则说明现已调用set_value发送数据,符号为退出状况
return _interruptedFuture.wait_for(std::chrono::milliseconds(0)) !=
std::future_status::timeout;
}
private:
std::promise<void> _interruptSignal;
std::future<void> _interruptedFuture;
};
}
#endif //LSERIALPORT_IWORKER_H
上面说到的ReadWriteWorker也是继承该类。在目标毁掉时,先告诉各个串口退出循环,然后运用std::thread::join堵塞并切换线程,等候每个线程完毕后,毁掉资源完结安全退出。
下面是封闭毁掉的功用函数ReadWriteWorker::interrupte()以及ReadWriteWorker::~ReadWriteWorker()
ReadWriteWorker::interrupte()
/**
* 退出使命
*/
void interrupte() override {
//监听器锁
std::lock_guard<std::mutex> llk(_mListenerMutex);
//音讯锁
std::lock_guard<std::mutex> lk(_mMsgMutex);
//发送退出信号
IWorker::interrupte();
_mMsgCond.notify_all();
}
ReadWriteWorker::~ReadWriteWorker()
ReadWriteWorker::~ReadWriteWorker() {
LOGE("---finishing worker---");
ReadWriteWorker::interrupte();
LOGE("wait for checkAvaliable thread end");
if ((_checkAvaliableThread != nullptr) && _checkAvaliableThread->joinable()) {
_checkAvaliableThread->join();//等候检查线程完毕
}
LOGE("wait for write thread end");
if ((_writeThread != nullptr) && _writeThread->joinable()) {
_writeThread->join();//等候写线程完毕
}
LOGE("wait for read thread end");
if ((_readThread != nullptr) && _readThread->joinable()) {
_readThread->join();//等候读线程完毕
}
LOGE("cleaning msgQueue");
//清空行列
while (!_mMsgQueue.empty()) {
_mMsgQueue.pop();
}
LOGE("cleaning thread ptr");
delete _checkAvaliableThread;
delete _writeThread;
delete _readThread;
_checkAvaliableThread = nullptr;
_writeThread = nullptr;
_readThread = nullptr;
LOGE("cleaning listener ptr");
if (_prepListener != nullptr) {
delete _prepListener;
_prepListener = nullptr;
}
if (_curlistener != nullptr) {
delete _curlistener;
_curlistener = nullptr;
}
LOGE("close SerialPort");
if (_serialPort != nullptr) {
LOGE("closing...");
_serialPort->Close();
delete _serialPort;
_serialPort = nullptr;
LOGE("close done!");
}
LOGE("finish done!");
}
}
5.多串口办理
前面现已封装好对串口操作读写的目标ReadWriteWorker,现在只需求运用一个办理类,对每个串口操作目标进行办理。存储这边咱们运用std::unordered_map以串口地址和串口操作目标作为键值对的方式进行保存,增加,删去等操作,运用std::unordered_map的优点在于它内部完结结构选用对键值进行哈希保存数据,不做任何排序,能对map中的单一值进行快速拜访,契合咱们现在的完结功用要求。
下面是多串口办理类LSerialPortManager声明定义
LSerialPortManager.h
#ifndef LSERIALPORT_LSERIALPORTMANAGER_H
#define LSERIALPORT_LSERIALPORTMANAGER_H
#include <unordered_map>
#include <jni.h>
#include "ReadWriteWorker.h"
using namespace mn::CppLinuxSerial;
namespace LSerialPort {
class LSerialPortManager {
public:
static JavaVM *jvm;
LSerialPortManager();
~LSerialPortManager();
/**
* 增加设备
* @param path 串口地址
* @param baudRate 波特率
* @param dataBits 数据位
* @param parity 校验位
* @param stopBits 中止位
* @param readIntervalTimeoutMills 循环读SerialPort超时时刻,超越时刻就持续下一次轮循
* @param checkIntervalWaitMills 循环检查等候时刻,假如想要回复多一些数据能够适当延长
* @return
*/
int addDevice(
std::string &path,
BaudRate &baudRate,
NumDataBits &dataBits,
Parity &parity,
NumStopBits &stopBits,
int32_t &readIntervalTimeoutMills,
long &checkIntervalWaitMills);
/**
* 删去设备
* @param path 串口地址
* @return
*/
int removeDevice(const std::string &path);
/**
* 检查串口是否被加载过
* @param path 串口地址
* @return
*/
bool hasDevice(const std::string &path);
/**
* 发送二进制音讯
* @param path 串口地址
* @param msg 二进制音讯
* @return
*/
int sendMessage(const std::string &path, const std::vector<uint8_t> &msg);
/**
* 设置串口数据监听器
* @param path 串口地址
* @param listener 监听器
* @return
*/
int setLSerialPortDataListener(const std::string &path, jobject *listener);
private:
//已加载的串口设备
std::unordered_map<std::string, std::unique_ptr<ReadWriteWorker>> _mDevices;
};
}
#endif //LSERIALPORT_LSERIALPORTMANAGER_H
因为篇幅原因,这儿不列出完结代码。假如要看具体完结能够检查ReadWriteWorker.cpp 。到此LSerialPort要害功用完结思路介绍完毕。
结语
最近学了点C++,总感觉需求上手撸点东西。正好公司项目有运用串口的场景,可是发现大多数串口库一般都需求运用者自己在上层完结对串口多线程办理读写,有些不便利。在网上发现个优异的C++底层完结多线程串口读写办理的库[MserialPort] github.com/flykule/Mse…) ,遂按照该库的代码思路写了LSerialPort。一方面巩固刚学习学习的C++知识,另一方面是重拾好久没用过的JNI、NDK。