preface
Long Long ago, the big Boss gave me a task, at first I was asked to cooperate with the third party and Android development to build a set of machines, and then the problem came, as we all know, there must be a lot of problems. For some reason, I somehow took up the banner of developing serial communication (ten thousand grass mud horse rushing ~), and by working on Github day and night, I could not find a suitable development framework, and then secretly comforted myself (maybe no one has time to do open source!). “, and then I went to the Android official website operation, can calculate to find some entrance, know the need to use NDK development, involving C++ development. ┭┮﹏┭┮ whoop ~ I seem to have gone to a fake college and returned everything I learned to the teacher.
Open book open book
Let’s get down to business. Let’s give you a sneak peek at the entrance. Let me tell you a little bit more.
SerialPortKit entrance
introduce
SerialPortKit is mainly based on the Android system to do the corresponding board serial port communication, generally customized development board, by the manufacturer to do the relevant customization. For example, RK3288 and RK3399
Just because I can’t find the relevant SDK doesn’t mean everyone can’t find it, ha ha ~, although there are many serial communication development frameworks in the last two years, each has its own advantages! Recently, I have also compiled a set of open source framework, and welcome you to use it and put forward valuable suggestions. If you can, please mention PR
The characteristics of
- Supports custom communication protocols
- Supports verification of communication addresses
- Supports the retry mechanism for sending failures
- Support one round one collection, one round multiple collection
- Multiple receiving commands are supported
- Switching serial ports
- Supports switching baud rate
- Supports specifying the maximum length of data to receive
- Supports send/receive timeout detection
- Support for main thread/child thread
- Supports multi-threaded concurrent communication
- You can customize sending tasks
- Instruction pool assembly is supported
- Support instruction tool
Wow ~ support so many functions ah!
How do you use it?
Wow ~ advantages say so many, so how to use it?
Add it in Project build.gradle
repositories {
maven {
name 'maven-snapshot'
url 'https://s01.oss.sonatype.org/content/repositories/snapshots/'}}Copy the code
Add it in app build.gradle
implementation 'IO. Making. Zhouhuandev: serial - port - kit - manage: - the SNAPSHOT 1.0.1' / / the require kotlin 1.7.0
Serial-port-kit-core can be used by developers who need to use data conversion tools or serial port search or fully customize data input and output
implementation 'IO. Making. Zhouhuandev: serial - port - kit - core: - the SNAPSHOT 1.0.1' / / is optional
Copy the code
Resource Android :attr/lStar not found
Gradle/caches/transforms - 2 / files - 2.1/3 c80c501edca1d8bdce41f94be0c4104 / core - 1.7.0 / res/values/values. The XML: 105-5-114:25: AAPT: error: resource android:attr/lStar not found.Copy the code
Because the Kotlin version of your current project is lower than 1.7.0, you need to forcibly replace the unified version
configurations.all {
resolutionStrategy {
force 'androidx. Core: the core - KTX: 1.6.0'}}Copy the code
More detailed or look at the source code!
SerialPortKit entrance
Instruction dispatch scheduler
We sent a directive that we should not release the main thread. Just kidding ~ time-consuming content should be put into the thread as much as possible. So here’s the problem! Sending an instruction and receiving a reply to that instruction completes a communication. How can we both send and receive?
An idea has been floating around in my mind lately, and it’s not just shrimp Guy’s AndroidStartUp that gives me some inspiration. Send instruction and receive instruction since a pair, so, can I send instruction and receive instruction together?
Now that the question has been thrown out, let’s get started.
Send instructions and receive instructions all do a Task, and then through the thread pool for scheduling processing.
SerialPortTask
internal interface SerialPortTask {
fun onTaskStart(a)
fun run(a)
fun onTaskCompleted(a)
fun mainThread(a): Boolean = false
}
Copy the code
Dispatches can be performed through the scheduler
fun dispatch(
task: BaseSerialPortTask,
onCompleted: ((task: BaseSerialPortTask) - >Unit)? = null
) {
if(task.mainThread()) { runOnUiThread { execute(task) onCompleted? .invoke(task) } }else{ mExecutor.execute { execute(task) onCompleted? .invoke(task) } } }private fun execute(task: BaseSerialPortTask) {
task.onTaskStart()
task.run()
task.onTaskCompleted()
}
Copy the code
Send and receive instruction data
Send and receive this pair of instructions to complete a communication, must be together ~
/** * Send data to serial port *@paramTask Sends data task */
fun sendBuffer(task: BaseSerialPortTask): Boolean {
xxx
...
var isSendSuccess = truemSerialPort? .apply { task.stream(outputStream = outputStream) } manager.dispatcher.dispatch(task) { isSendSuccess = it.isSendSuccessif (isSendSuccess) {
it.sendTime = System.currentTimeMillis()
}
}
if(! isSendSuccess) { xxx ... }else {
if(! tasks.contains(task)) { tasks.add(task) } }return isSendSuccess
}
Copy the code
Once the Task is fetched, the mSerialPort is used to fetch the output stream, load it into the Task, and use the scheduler to distribute the Task so that the instruction is sent.
After the instruction is sent successfully, the sending time is recorded and loaded into the task queue tasks, so that the data can be received later for instruction matching and timeout detection.
The next step is to process the received instructions
/** * Received serial port data **@paramData Callback data */
fun sendMessage(data: WrapReceiverData) {
readLock.lock()
try {
if(invalidTasks.isNotEmpty()) invalidTasks.clear() tasks.forEach { task -> manager.config.addressCheckCall? .let {if (it.checkAddress(task.sendWrapData(), data)) {
onSuccess(task, data)}}? : onSuccess(task,data)}// Remove invalid tasks
invalidTasks.forEach { task ->
tasks.remove(task)
}
} finally {
readLock.unlock()
}
}
Copy the code
After receiving the data from the serial port, tasks are matched by traversing the current Task queue, detecting the communication protocol address, and then the data is distributed. However, if there is a mismatch, the default policy is to distribute the data back to all tasks that sent the command. But it’s very, very, very not recommended. It is possible to violate the number of times of communication (in extreme cases, I send an instruction concurrently and only receive the data for one instruction, while the other instruction is left in the queue for dispatch and is captured by the timeout mechanism to report a timeout).
Another scenario, which may also be involved, is that an instruction is sent, but the data comes back because of normal interaction, similar to TCP’s three-way handshake.
I send a command, such as: 0xAA 0xA1 0x00 0xB5, and then the next machine immediately reply to a received command 0xAA 0xA1 0x00 0xB5 represents “normal communication, I have received the command, I now want to go to the process ~”, and then the processing result data again returned. It’s a one-to-many relationship.
private fun onSuccess(task: BaseSerialPortTask.data: WrapReceiverData) {
if (task.receiveCount < manager.config.receiveMaxCount) {
task.receiveCount++
task.waitTime = System.currentTimeMillis()
switchThread(task) {
task.onDataReceiverListener().onSuccess(data.apply {
duration = abs(task.waitTime - task.sendTime)
})
}
if (task.receiveCount == manager.config.receiveMaxCount) {
invalidTasks.add(task)
}
} else {
invalidTasks.add(task)
}
}
Copy the code
Process the Task by comparing the current Task with the ReceiveMaxCount configured in Config. Then switch threads and send callback.
Communication protocol custom verification
Most of the time, we are customized with hardware engineers communication protocol scheme, then the problem comes, how do I filter and verify the accuracy of the data! How do you do subsequent distribution? Ha ha ~ don’t panic, the author has considered it!
SerialPortKit.newBuilder(this)
// Whether to customize the correctness of data sent by the lower computer and load the verified Byte array into WrapReceiverData
.isCustom(true.object : OnDataCheckCall {
override fun customCheck(
buffer: ByteArray,
size: Int,
onDataPickCall: (WrapReceiverData) - >Unit
): Boolean {
onDataPickCall.invoke(WrapReceiverData(buffer, size))
return true}})Copy the code
You can add custom receive callbacks to do the corresponding filtering and rule processing. Ondatapickcall.invoke (WrapReceiverData(buffer, size)) call back to continue processing.
Communication protocol address matching verification
When we receive the instruction sent by the lower machine, we need to match the Task, so it is better to match the address bit or command bit.
SerialPortKit.newBuilder(this)
// Check the address bits of sending and receiving instructions, if the same, it is a normal communication
.addressCheckCall(object : OnAddressCheckCall {
override fun checkAddress(
wrapSendData: WrapSendData,
wrapReceiverData: WrapReceiverData
): Boolean {
return wrapSendData.sendData[1] == wrapReceiverData.data[1]}})Copy the code
I personally recommend implementing address matching very, very, very much.
Customize tasks to send data
Each instruction is sent, at the bottom, in a separate Task execution, without interference. A user-defined Task inherits its parent BaseSerialPortTask class. It can monitor the operations performed before sending tasks start or after sending tasks finish. You can toggle the current sending task and the final OnDataReceiverListener to hear whether the callback is executed on the main thread. The default is to execute and call back in child threads.
The custom Task
class SimpleSerialPortTask(
private val wrapSendData: WrapSendData,
private val onDataReceiverListener: OnDataReceiverListener
) : BaseSerialPortTask() {
override fun sendWrapData(a): WrapSendData = wrapSendData
override fun onDataReceiverListener(a): OnDataReceiverListener = onDataReceiverListener
override fun onTaskStart(a){}override fun onTaskCompleted(a){}override fun mainThread(a): Boolean {
return false}}Copy the code
Send a Task
MyApp.portManager? .send(SimpleSerialPortTask(WrapSendData(SenderManager.getSender().sendStartDetect()),object : OnDataReceiverListener {
override fun onSuccess(data: WrapReceiverData) {
Log.d(TAG, "Response data:${TypeConversion.bytes2HexString(data.data)}")}override fun onFailed(wrapSendData: WrapSendData, msg: String) {
Log.e(
TAG,
"Send data:${TypeConversion.bytes2HexString(wrapSendData.sendData)}.$msg")}override fun onTimeOut(a) {
Log.e(TAG, "Send or receive timeout")}}))Copy the code
Timeout detection
It is possible that the sent command has not received data or received data, but the last data has timed out. In this case, it is an invalid Task and needs to be removed.
/** * Check the timeout invalid task */
fun checkTimeOutTask(a) {
readLock.lock()
try {
if (invalidTasks.isNotEmpty()) invalidTasks.clear()
tasks.forEach { task ->
if (isTimeOut(task)) {
switchThread(task) {
task.onDataReceiverListener().onTimeOut()
}
invalidTasks.add(task)
}
}
// Remove invalid tasks
invalidTasks.forEach { task ->
tasks.remove(task)
}
} finally {
readLock.unlock()
}
}
/** * Check whether timeout */
private fun isTimeOut(task: BaseSerialPortTask): Boolean {
val currentTimeMillis = System.currentTimeMillis()
return if (task.waitTime == 0L) {
// Indicates that the data has not been received
val sendOffset = abs(currentTimeMillis - task.sendTime)
sendOffset > task.sendWrapData().sendOutTime
} else {
// Some data has been received, but the last data has timed out
val waitOffset = abs(currentTimeMillis - task.waitTime)
waitOffset > task.sendWrapData().waitOutTime
}
}
Copy the code
Retry mechanism
When sending an instruction fails, the retry mechanism is activated.
/** * Send data to serial port *@paramTask Sends data task */
fun sendBuffer(task: BaseSerialPortTask): Boolean {
xxx
...
if(! isSendSuccess) {// Restart the serial port and resend the command.onRetryCall? .let {if (it.retry()) {
it.call(task)
} else {
switchThread(task) {
task.onDataReceiverListener()
.onFailed(
task.sendWrapData(),
"Failed to send, retried ${manager.retryCount} time.")}}}}else {
if(! tasks.contains(task)) { tasks.add(task) } }return isSendSuccess
}
Copy the code
At this point, the current serial port will be reopened and resend.
helper.onRetryCall = object : OnRetryCall {
override fun retry(a): Boolean = retryCount in 1..MAX_RETRY_COUNT
override fun call(task: BaseSerialPortTask) {
if (config.debug) {
Log.d(TAG, "Retry opening the serial port for ${retryCount++}ed!")}if (open()) {
send(task)
}
}
}
Copy the code
Switch serial port & baud rate
It is possible that the process of development will involve switching the serial port or switching the baud rate, so sure to provide such API to the majority of friends! That is my deep love for everyone!
Direct operation, no problem.
@JvmOverloads
fun switchDevice(path: String = config.path, baudRate: Int = config.baudRate): Boolean{ check(path ! ="") { "Path is must important parameters, and it cannot be null!" }
check(baudRate >= 0) { BaudRate is must important parameters, and it cannot be less than 0! }
config.path = path
config.baudRate = baudRate
return open()}Copy the code
conclusion
I don’t know if it suits everyone’s taste! This is also I thought about for a long time, even sleep can not sleep, still want to roll out for everyone, ao ~ no, it is open source. Please also mention issues and PR if there are any shortcomings.
Welcome to the Start SerialPortKit portal