前言
最近在做一款产品,主要功能是android 接收到音频数据通过串口实时发送MCU处理,要求延迟小,速度快,传输稳定性有保障。我们都知道串口是一种不可信任性传输。不能保障传输数据完整性,在产品测试过程中也发现低概率性出现数据丢包现象。所以需要设计一种支持流式可靠性传输协议。
产品需要Android串口跟MCU串口通信传输音频流,每次传输大概1M左右,MCU接收到音频后需要立马处理,所以需要尽可能快接收数据处理,不能接收每一帧会回传校验结果。只有等到有错误帧才返回给Android 请求重发错误帧,如果一次性传输1M,体积过大,时间过程长,如果数据有误需要重传1M数据,延时性太大。所以拆包每次发送10K数据发送。
所以设计了一种流式传输且支持重传的串口协议,支持传输数据大小是1B ~ 1G , 传输数据帧大小10K,小于10K按照实际大小发送,大于10K则拆包发送。比如100K分成10包发送。正常发送不必回传结果,只有错误才回传错误帧的index,比如发送到低5包出错,则回传给Android 第5帧错误。Android重新从5包发送数据。
协议大概流程图如图所示。
既然是数据传输必然有传输格式协议,使用简单的TLV协议即可。
传输协议简介如下:
包结构
Header:包头
Payload:有效载荷,Module_id+Msg_id+Data
Checksum:校验和,校验数据为Header + Payload
StartFlag:固定值0xFFAA5500,每一个数据包固定的开始序列
Handle:未使用,默认0
Version:0x01 协议版本,第一个版本
Length: payload区域的长度+ Checksum字段
Opcode:
Opcode 方法定义 描述
0x01 request Request需要和response配合使用,每个request都必须有一个response的应答。Request可以由主 设备(主机)发起,也可以由从设备(翻译机、副机)发起。
0x02 response 回复对应的request的response
0x03 indication 主设备推送消息给从设备
0x04 notification 从设备推送消息给主设备
下面就是代码讲解:
协议对我提供两个接口 send() 和 flush 两个接口。和一个回调接口OnSendCallback。
已一个传输文件为例
send 将文件保存到缓存中,此时并没有发送。//发送缓存大小可修改,当前定义4M
flush 将缓存文件拆包通过串口发送 ,是真正的发送文件
先放demo 的文件
package com.dfxh.wang.serialport_test;
import android.Manifest;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.media.MediaPlayer;
import android.os.BatteryManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import static android.text.InputType.TYPE_CLASS_NUMBER;
public class MainActivity extends AppCompatActivity implements SerialPortUtils.OnSendCallback,View.OnClickListener {
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}
private final String TAG = "MainActivity";
private int looptime = 100;
private Context mContext;
private boolean flag = true;
private String mFileName = "/sdcard/receive.bin";
private String mSendFileName = "/sdcard/send.bin";
private String mSendFileName1 = "/sdcard/test.txt";
String path ="/storage/emulated/0/testFolder/testFile";
private CustomServiceDataHelper mDtaHelper = new CustomServiceDataHelper();
public static SerialPortUtils serialPortUtils;
private static Lock lock = new ReentrantLock();
private long receiveFileLength = 0;
private ExecutorService singleThreadExecutor;
private int send_index = 1;
private int sendtime = 0;
BatteryManager batteryManager;
private boolean charging = false;
private boolean no_enough_space_flag = false;
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 2://通知发送方错误帧id
// serialPortUtils.sendErorPackIndex();
break;
case 3://通知发送方错误帧id
serialPortUtils.sendErorPackIndex();
break;
case 7:
//重新发送错误数据包
short index = (short) msg.arg1;
ReSendErrorData(index);
Log.d(TAG, "mHandler resend data index= " + index);
break;
case 8:
Log.d("SendData", "mHandler send new data =====");
if ((sendtime <1000)&&(!no_enough_space_flag)){
Log.d(TAG, "send file time : " + sendtime);
// startSendBinFile1(sendtime);
startSendBinFile();
sendtime++;
}else {
Log.d(TAG, "no_enough_space_flag: " + no_enough_space_flag);
}
break;
case 11:
led_red_on();
break;
case 12:
led_green_on();
break;
}
}
};
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mContext = this;
setContentView(R.layout.activity_main);
Log.e(TAG,"MainActivity onCreate ====================");
TextView tv = findViewById(R.id.lv);
serialPortUtils = new SerialPortUtils(mHandler,this);
serialPortUtils.setSendCallback(this);
singleThreadExecutor = Executors.newSingleThreadExecutor();
Button open_bt = (Button) findViewById(R.id.open_bt);
open_bt.setOnClickListener(this);
Button send_bt = (Button) findViewById(R.id.send_bt);
send_bt.setOnClickListener(this);
Button close_bt = (Button) findViewById(R.id.close);
close_bt.setOnClickListener(this);
IntentFilter mIntentFilter = new IntentFilter();
mIntentFilter.addAction(Intent.ACTION_BATTERY_CHANGED);
mIntentFilter.addAction(Intent.ACTION_BATTERY_LOW);
mIntentFilter.addAction(Intent.ACTION_BATTERY_OKAY);
mIntentFilter.addAction(Intent.ACTION_POWER_CONNECTED);
mIntentFilter.addAction(Intent.ACTION_POWER_DISCONNECTED);
registerReceiver(mIntentReceiver, mIntentFilter);
batteryManager = (BatteryManager)getSystemService(BATTERY_SERVICE);
//new StatuThread().start();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Android M Permission check
if (this.checkSelfPermission(Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[]{
Manifest.permission.READ_PHONE_STATE}, 1);
}
}
}
public void unregister() {
if (mIntentReceiver != null) {
this.unregisterReceiver(mIntentReceiver);
}
}
private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent != null) {
int status = 0;
String action = intent.getAction();
int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
float batteryPct = level / (float)scale;
Log.e(TAG, "level --> " + level);
switch (action) {
case Intent.ACTION_BATTERY_CHANGED://电量发生改变
Log.e(TAG, "BatteryBroadcastReceiver --> onReceive--> ACTION_BATTERY_CHANGED");
//Toast.makeText(mContext,"电量变化",Toast.LENGTH_SHORT).show();
if(charging){
if(level < 95){
status = 11;
}else{
status = 12;
}
}else {
if(0 < level && level < 10){
status = 11;
}else{
status = 12;
}
}
Toast.makeText(mContext,"充电状态 " + charging + "值: " + level,Toast.LENGTH_SHORT).show();
break;
case Intent.ACTION_BATTERY_LOW://电量低
Log.e(TAG, "BatteryBroadcastReceiver --> onReceive--> ACTION_BATTERY_LOW");
status = 9;
break;
case Intent.ACTION_BATTERY_OKAY://电量充满
Log.e(TAG, "BatteryBroadcastReceiver --> onReceive--> ACTION_BATTERY_OKAY");
status = 12;
break;
case Intent.ACTION_POWER_CONNECTED://接通电源
Log.e(TAG, "BatteryBroadcastReceiver --> onReceive--> ACTION_POWER_CONNECTED");
if(level < 95){
status = 11;
}else{
status = 12;
}
charging = true;
Toast.makeText(mContext,"充电中,电量是" + level,Toast.LENGTH_SHORT).show();
break;
case Intent.ACTION_POWER_DISCONNECTED://拔出电源
Log.e(TAG, "BatteryBroadcastReceiver --> onReceive--> ACTION_POWER_DISCONNECTED");
charging = false;
if(0 < level && level < 10){
status = 11;
}else{
status = 12;
}
// Toast.makeText(mContext,"未充电" + level,Toast.LENGTH_SHORT).show();
break;
}
Message msg = new Message();
msg.what = status;
mHandler.sendMessage(msg);
}
}
};
@Override
protected void onResume() {
super.onResume();
serialPortUtils.openSerialPort();
}
private void readBinfile(){
File file = new File(mSendFileName);
FileInputStream fileInputStream = null;
try {
byte[] buf = new byte[1024*512];
int length = -1;
fileInputStream = new FileInputStream(file);
while(((length = fileInputStream.read(buf)) != -1)&&(no_enough_space_flag!=true)){
if(serialPortUtils.send(buf,length)==false){
no_enough_space_flag = true;
break;
}
// PrintfInfo(buf,length,"buf");
Log.d("SendData", "send_index= " + send_index);
Log.d("SendData", toHexString(buf,length));
send_index++;
serialPortUtils.flush();
}
fileInputStream.close();
// serialPortUtils.sendFileEndFlag();
send_index =0;
Log.d("SendData", "发送完毕=====");
Thread.sleep(3000);
}catch (Exception e){
e.printStackTrace();
}
}
@Override
public void OnResult(int ret) {
}
@Override
public void Onread(CommonResponseData helper) {
Log.d(TAG, "Onread Opcode=====" + helper.getOpcode());
Log.d(TAG, "Onread ModuleId=====" + helper.getModuleId());
Log.d(TAG,