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