蓝牙串口无线烧写STM32程序的安卓APP,版本V1.0,目前仅支持CC2541蓝牙模块。安卓9和10系统下测试没问题。
开发工具:Android Studio 3.5.2 下载地址:百度网盘 请输入提取码(提取码:mzf5)
实现程序内容发送的核心代码为DownloadThread类的run方法。
安卓Java采用的是大端序,而STM32单片机C语言采用的是小端序,在Java中由Integer.reverseBytes方法实现大小端序的相互转换。
【主要代码】
app/src/main/java/com/oct1158/uartdfu/MainActivity:
package com.oct1158.uartdfu;
import android.Manifest;
import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothAdapter.LeScanCallback;
import android.bluetooth.BluetoothDevice;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Handler.Callback;
import android.os.Message;
import android.os.Parcelable;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;
import android.widget.SimpleAdapter;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.widget.TextViewCompat;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.HashMap;
public class MainActivity extends AppCompatActivity implements Callback, LeScanCallback, OnItemClickListener {
private static final int MSG_STOP_SCAN = 1;
private static final int REQUEST_ENABLE_BLUETOOTH = 1;
private static final int REQUEST_GRANT_PERMISSION = 2;
private boolean scanning;
private long scanningTime;
private ArrayList<HashMap<String, String>> deviceList = new ArrayList<HashMap<String, String>>();
private BluetoothAdapter bluetoothAdapter;
private Handler handler;
private HashMap<String, BluetoothDevice> devices = new HashMap<String, BluetoothDevice>();
private ListView listView;
private SimpleAdapter simpleAdapter;
private boolean getPermission() {
if (!bluetoothAdapter.isEnabled()) {
Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
startActivityForResult(intent, REQUEST_ENABLE_BLUETOOTH);
return false;
} else if (Build.VERSION.SDK_INT >= 23) {
String[] permissions = {Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION};
for (String permission : permissions) {
int status = checkSelfPermission(permission);
if (status != PackageManager.PERMISSION_GRANTED) {
// 申请权限
requestPermissions(permissions, REQUEST_GRANT_PERMISSION);
return false;
}
}
}
return true;
}
@Override
public boolean handleMessage(@NonNull Message message) {
long now = System.currentTimeMillis();
switch (message.what) {
case MSG_STOP_SCAN:
if (now - scanningTime > 8000) {
stopScan(false);
}
return true;
default:
return false;
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
switch (requestCode) {
case REQUEST_ENABLE_BLUETOOTH:
if (resultCode == Activity.RESULT_CANCELED) {
ToastUtils.show(this, "蓝牙未打开");
} else {
startScan();
}
break;
}
super.onActivityResult(requestCode, resultCode, data);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ActionBar supportActionBar = getSupportActionBar();
supportActionBar.setDisplayShowHomeEnabled(true);
supportActionBar.setIcon(R.mipmap.ic_launcher2);
String[] keys = {"name", "addr"};
int[] ids = {android.R.id.text1, android.R.id.text2};
simpleAdapter = new SimpleAdapter(this, deviceList, android.R.layout.simple_list_item_2, keys, ids) {
/* 第一行改成大字体 */
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = super.getView(position, convertView, parent);
TextView text1 = view.findViewById(android.R.id.text1);
TextViewCompat.setTextAppearance(text1, R.style.TextAppearance_AppCompat_Large);
return view;
}
};
listView = findViewById(R.id.listView);
listView.setAdapter(simpleAdapter);
listView.setOnItemClickListener(this);
PackageManager packageManager = getPackageManager();
if (!packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
ToastUtils.show(this, "设备不支持低功耗蓝牙");
finish();
}
handler = new Handler(this);
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter == null) {
ToastUtils.show(this, "设备不支持蓝牙");
finish();
}
if (savedInstanceState == null) {
startScan();
} else {
// 直接从保存的结果里读取
// 请注意不能直接将Parcelable[]强转为BluetoothDevice[], 否则可能会抛出异常
// 只能遍历数组, 一个一个地转换
Parcelable[] arr = savedInstanceState.getParcelableArray("devices");
if (arr != null) {
for (int i = 0; i < arr.length; i++) {
BluetoothDevice device = (BluetoothDevice)arr[i];
onLeScan(device, 0, null);
}
}
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
HashMap<String, String> info = deviceList.get(position);
String addr = info.get("addr");
BluetoothDevice device = devices.get(addr);
if (device != null) {
Intent intent = new Intent(this, DownloadActivity.class);
intent.putExtra("device", device);
startActivity(intent);
}
}
@Override
public void onLeScan(@NotNull BluetoothDevice device, int rssi, byte[] scanRecord) {
String name = device.getName();
String addr = device.getAddress();
if (!devices.containsKey(addr)) {
devices.put(addr, device);
HashMap<String, String> info = new HashMap<String, String>();
info.put("name", name);
info.put("addr", addr);
deviceList.add(info);
simpleAdapter.notifyDataSetChanged();
}
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
int id = item.getItemId();
switch (id) {
case R.id.action_scan:
startScan();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
protected void onPause() {
stopScan(true);
super.onPause();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case REQUEST_GRANT_PERMISSION:
int i;
for (i = 0; i < grantResults.length; i++) {
if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
break;
}
}
if (i == grantResults.length) {
startScan(); // 重新开始搜索
} else {
ToastUtils.show(this, "蓝牙设备搜索权限未打开");
}
break;
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
/* 旋转屏幕前保存状态 */
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// 保存设备列表 (保持原有顺序)
int size = deviceList.size();
if (size > 0) {
BluetoothDevice[] arr = new BluetoothDevice[size];
for (int i = 0; i < size; i++) {
HashMap<String, String> info = deviceList.get(i);
String addr = info.get("addr");
arr[i] = devices.get(addr);
}
outState.putParcelableArray("devices", arr);
}
}
private void startScan() {
if (!scanning) {
devices.clear();
deviceList.clear();
simpleAdapter.notifyDataSetChanged();
if (getPermission()) {
scanning = bluetoothAdapter.startLeScan(this);
if (scanning) {
ToastUtils.show(this, "开始搜索设备");
scanningTime = System.currentTimeMillis();
handler.sendEmptyMessageDelayed(MSG_STOP_SCAN, 10000);
} else {
ToastUtils.show(this, "搜索设备失败");
}
}
}
}
private void stopScan(boolean forced) {
if (scanning) {
bluetoothAdapter.stopLeScan(this);
if (forced) {
ToastUtils.show(this, "停止搜索设备");
} else {
ToastUtils.show(this, "设备搜索完毕");
}
scanning = false;
}
}
}
app/src/main/java/com/oct1158/uartdfu/DownloadActivity.java:
package com.oct1158.uartdfu;
import android.Manifest;
import android.bluetooth.BluetoothDevice;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Handler.Callback;
import android.os.Message;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import org.jetbrains.annotations.NotNull;
public class DownloadActivity extends AppCompatActivity implements Callback, TextWatcher, DialogInterface.OnClickListener, View.OnClickListener {
public static final int MSG_SHOW_TOAST = 1;
public static final int MSG_SHOW_TEXT = 2;
public static final int MSG_SHOW_FILE_INFO = 3;
public static final int MSG_SET_PROGRESS = 4;
public static final int MSG_ALERT = 5;
public static final int MSG_THREAD_END = 6;
private static final int REQUEST_OPEN_FILE = 1;
private static final int REQUEST_GRANT_PERMISSION = 2;
private static DownloadThread thread;
private boolean fileValid;
private boolean savedInstance;
private long exitTime;
private BluetoothDevice device;
private Button button1, button2;
private EditText editText;
private Handler handler;
private ProgressBar progressBar;
private TextView textView1, textView2;
@Override
public void afterTextChanged(Editable s) {
}
public void alert(String msg, String title) {
if (progressBar.isIndeterminate()) {
progressBar.setIndeterminate(false);
textView2.setText(null);
}
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(title);
builder.setMessage(msg);
builder.setPositiveButton("确定", this);
builder.setCancelable(false); // 点对话框以外的地方不起作用
AlertDialog dlg = builder.create();
dlg.show();
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
private void enableControls(boolean enabled) {
editText.setEnabled(enabled);
button1.setEnabled(enabled);
button2.setEnabled(enabled && fileValid);
if (enabled) {
progressBar.setIndeterminate(false);
textView2.setText(R.string.tip_download);
}
}
private boolean getPermission(boolean request) {
if (Build.VERSION.SDK_INT >= 23) {
String[] permissions = {Manifest.permission.READ_EXTERNAL_STORAGE};
int status = checkSelfPermission(permissions[0]);
if (status != PackageManager.PERMISSION_GRANTED) {
// 申请权限
if (request) {
requestPermissions(permissions, REQUEST_GRANT_PERMISSION);
}
return false;
}
}
return true;
}
@Override
public boolean handleMessage(@NonNull Message message) {
switch (message.what) {
case MSG_SHOW_TOAST:
ToastUtils.show(this, (CharSequence)message.obj, message.arg1);
break;
case MSG_SHOW_TEXT:
textView2.setText((CharSequence)message.obj);
break;
case MSG_SHOW_FILE_INFO:
updateFileInfo((HexFile)message.obj);
break;
case MSG_SET_PROGRESS:
if (message.arg1 >= 0 && message.arg1 <= 100) {
if (message.arg1 == 0) {
textView2.setText("已检测到设备");
} else {
textView2.setText("进度: " + message.arg1 + "%");
}
progressBar.setIndeterminate(false);
progressBar.setProgress(message.arg1);
}
break;
case MSG_ALERT:
String msg = (String)message.obj;
thread = null; // 弹窗, 说明线程已经执行完毕了
if (msg == null || msg.isEmpty()) {
alert("未知错误", "下载固件");
} else {
alert(msg, "下载固件");
}
break;
case MSG_THREAD_END:
enableControls(true);
thread = null;
break;
}
return false;
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
savedInstance = false; // 从其他Activity返回, 也要清除这个标志
switch (requestCode) {
case REQUEST_OPEN_FILE:
if (resultCode == RESULT_OK) {
Uri uri = data.getData();
String path = FileUtils.getPath(this, uri);
if (path == null) {
ToastUtils.show(this, "获取文件路径失败");
break;
}
editText.setText(path);
HexFile hexFile = new HexFile(path);
String msg = hexFile.getErrorMessage();
if (msg != null) {
ToastUtils.show(this, msg);
}
updateFileInfo(hexFile);
}
break;
}
super.onActivityResult(requestCode, resultCode, data);
}
@Override
public void onClick(DialogInterface dialog, int which) {
// 关闭了警告消息对话框
enableControls(true);
}
@Override
public void onClick(View view) {
int id = view.getId();
switch (id) {
case R.id.button:
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
intent.addCategory(Intent.CATEGORY_OPENABLE);
startActivityForResult(intent, REQUEST_OPEN_FILE);
break;
case R.id.button2:
enableControls(false);
progressBar.setIndeterminate(true);
String path = editText.getText().toString();
thread = new DownloadThread(device, this, handler, path);
thread.start();
break;
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_download);
ActionBar supportActionBar = getSupportActionBar();
supportActionBar.setDisplayHomeAsUpEnabled(true);
Intent intent = getIntent();
Bundle extras = intent.getExtras();
device = (BluetoothDevice)extras.get("device");
String name = device.getName();
String addr = device.getAddress();
if (name != null && !name.isEmpty()) {
setTitle(name + " (" + addr + ")");
} else {
setTitle(addr);
}
button1 = findViewById(R.id.button);
button1.setOnClickListener(this);
button2 = findViewById(R.id.button2);
button2.setOnClickListener(this);
editText = findViewById(R.id.editText);
editText.addTextChangedListener(this);
progressBar = findViewById(R.id.progressBar);
textView1 = findViewById(R.id.textView);
textView2 = findViewById(R.id.textView2);
handler = new Handler(this);
if (!getPermission(true)) {
enableControls(false);
}
}
@Override
protected void onDestroy() {
if (thread != null && !savedInstance) {
thread.handler = null;
thread.cancel();
thread = null;
ToastUtils.show(this, "固件下载已中止");
}
super.onDestroy();
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
if (thread != null && thread.isAlive()) {
long now = System.currentTimeMillis();
if (now - exitTime > 3000) {
exitTime = now;
savedInstance = false;
ToastUtils.show(this, "请再按一次返回键中止固件下载");
return false;
}
// 返回时, 会关闭当前Activity, 执行onDestroy方法, 在onDestroy方法里面清理thread线程
}
}
return super.onKeyDown(keyCode, event);
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
int id = item.getItemId();
switch (id) {
case android.R.id.home:
savedInstance = false; // 保证退出时能够清理到thread线程, 以防万一
finish();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case REQUEST_GRANT_PERMISSION:
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
enableControls(true);
} else {
ToastUtils.show(this, "文件读取权限未打开");
}
break;
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
/* 旋转屏幕后恢复状态 */
// 旋转屏幕后 (或者从其他很耗内存的APP返回到本APP后), 原Activity会被销毁(onDestroy)然后重建(onCreate)
// 文本输入框的内容和下载按钮的启用状态不变, 然而textView文字会消失
@Override
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
savedInstance = false;
textView1.setText(savedInstanceState.getString("textView1"));
textView2.setText(savedInstanceState.getString("textView2"));
if (savedInstanceState.getBoolean("indeterminate")) {
progressBar.setIndeterminate(true);
}
if (getPermission(false)) {
fileValid = savedInstanceState.getBoolean("fileValid");
enableControls(savedInstanceState.getBoolean("enabled"));
if (thread == null) {
if (savedInstanceState.getBoolean("threadAlive")) {
alert("固件下载已中止", "下载固件");
} else {
enableControls(true);
}
} else {
if (thread.isAlive()) {
thread.handler = handler;
} else {
boolean complete = thread.isComplete();
thread = null;
if (!complete) {
alert("固件下载未完成", "下载固件");
} else {
enableControls(true);
}
}
}
} else {
// 按钮的禁用状态可能已被super.onRestoreInstanceState改变, 这里需要再禁用一次
enableControls(false);
}
}
/* 旋转屏幕前保存状态 */
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
savedInstance = true;
outState.putString("textView1", textView1.getText().toString());
outState.putString("textView2", textView2.getText().toString());
outState.putBoolean("indeterminate", progressBar.isIndeterminate());
outState.putBoolean("fileValid", fileValid);
outState.putBoolean("enabled", button1.isEnabled());
outState.putBoolean("threadAlive", thread != null && thread.isAlive() && !thread.isComplete());
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
String str = s.toString();
str = str.trim();
if (!str.isEmpty()) {
button2.setEnabled(true);
fileValid = true;
} else {
button2.setEnabled(false);
fileValid = false;
}
}
private void updateFileInfo(@NotNull HexFile hexFile) {
Resources resources = getResources();
String description = resources.getString(R.string.text_description);
if (hexFile.getError() == HexFile.ERR_NONE) {
String[] arr = description.split("\n");
int size = hexFile.getSize();
arr[0] += " " + HexFile.toKBSize(size) + " (" + size + "字节)";
arr[1] += " " + HexFile.toHexAddress(hexFile.getStartAddress()) + " - " + HexFile.toHexAddress(hexFile.getEndAddress());
arr[2] += " " + HexFile.toHexAddress(hexFile.getEntry());
description = TextUtils.join("\n", arr);
fileValid = true;
} else {
button2.setEnabled(false);
fileValid = false;
}
textView1.setText(description);
}
}
app/src/main/java/com/oct1158/uartdfu/DownloadThread.java:
package com.oct1158.uartdfu;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.os.Handler;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
public class DownloadThread extends Thread {
public static final int HEADER_FIRMWARE_INFO = 0x32f103c8;
public static final int POLYNOMIAL_CRC8 = 0x107;
private boolean cancelled;
private boolean complete;
private BluetoothDevice device;
private Context context;
private String path;
public Handler handler;
public DownloadThread(@NotNull BluetoothDevice device, @NotNull Context context, Handler handler, @NotNull String path) {
super();
this.device = device;
this.context = context;
this.handler = handler;
this.path = path;
}
@Contract(pure = true)
public static int calcCRC8(byte[] b) {
return calcCRC8(b, 0, b.length);
}
@Contract(pure = true)
public static int calcCRC8(byte[] b, int off, int len) {
short temp = 0;
if (len != 0) {
temp = (short)(b[off] << 8);
}
for (int i = 1; i <= len; i++) {
if (i != len) {
temp |= b[off + i] & 0xff;
}
for (int j = 0; j < 8; j++) {
if ((temp & 0x8000) != 0) {
temp ^= POLYNOMIAL_CRC8 << 7;
}
temp <<= 1;
}
}
return (temp >> 8) & 0xff;
}
public void cancel() {
cancelled = true;
interrupt();
}
public boolean isComplete() {
return complete;
}
@Override
public void run() {
try (
DeviceConnection conn = new DeviceConnection(device, context);
InputStream in = conn.getInputStream();
OutputStream out = conn.getOutputStream();
) {
handler.obtainMessage(DownloadActivity.MSG_SHOW_TEXT, "正在检测设备...").sendToTarget();
boolean connected = conn.waitForConnection(5000);
if (connected) {
// 检测设备
byte[] data = new byte[16];
int i, len;
conn.setTimeout(100);
while (!cancelled) {
Arrays.fill(data, (byte)0xab);
out.write(data);
try {
len = in.read(data);
} catch (IOException e) {
len = 0;
}
if (len == data.length) {
for (i = 0; i < len; i++) {
if (data[i] != (byte)0xcd) {
break;
}
}
if (i == len) {
break; // 已检测到设备
}
}
}
if (cancelled) {
throw new Exception();
}
// 读取HEX文件并发送HEX文件信息
handler.obtainMessage(DownloadActivity.MSG_SET_PROGRESS, 0, 0).sendToTarget();
HexFile hexFile = new HexFile(path);
handler.obtainMessage(DownloadActivity.MSG_SHOW_FILE_INFO, hexFile).sendToTarget();
String msg = hexFile.getErrorMessage();
if (msg != null) {
throw new Exception(msg);
}
conn.setTimeout(10000);
ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
DataOutputStream bytesDataOut = new DataOutputStream(bytesOut);
bytesDataOut.writeInt(Integer.reverseBytes(HEADER_FIRMWARE_INFO));
bytesDataOut.writeInt(Integer.reverseBytes(hexFile.getSize()));
bytesDataOut.writeInt(Integer.reverseBytes(hexFile.getStartAddress()));
bytesDataOut.writeInt(Integer.reverseBytes(hexFile.getStartAddress() + hexFile.getSize()));
bytesDataOut.writeInt(Integer.reverseBytes(hexFile.getEntry()));
bytesDataOut.writeByte(calcCRC8(hexFile.getData())); // firmware_checksum
bytesDataOut.writeByte(calcCRC8(bytesOut.toByteArray())); // header_checksum
bytesDataOut.close();
bytesOut.writeTo(out);
byte[] response = new byte[9];
boolean downloading = false;
while (!cancelled) {
len = in.read(response);
if (len != response.length || calcCRC8(response) != 0) {
throw new IOException();
}
ByteArrayInputStream bytesIn = new ByteArrayInputStream(response);
DataInputStream bytesDataIn = new DataInputStream(bytesIn);
int addr = Integer.reverseBytes(bytesDataIn.readInt());
int size = Integer.reverseBytes(bytesDataIn.readInt());
bytesDataIn.close();
if (addr == 0) {
throw new Exception(String.format("固件大小超过设备Flash容量: %.1fKB", size / 1024.0));
} else if (addr == 0xffffffff && size == 0xffffffff) {
// 重新发送固件信息
bytesOut.writeTo(out);
continue;
} else if (!downloading && addr != hexFile.getStartAddress()) {
throw new Exception("该固件不适用于此设备"); // 固件起始地址无效
} else if (size == 0) {
break; // 发送结束
} else if (size == 0xffffffff) {
continue; // 继续等待 (比如擦除Flash需要很长时间, 为了避免提示超时, 可使用这个指令)
} else if (addr - hexFile.getStartAddress() + size > hexFile.getSize()) {
throw new Exception("设备出现异常");
} else {
downloading = true;
}
data = hexFile.getData(addr - hexFile.getStartAddress(), size);
out.write(data);
double progress = (double)(addr - hexFile.getStartAddress() + size) / hexFile.getSize();
i = (int)(progress * 100 + 0.5);
if (i > 100) {
i = 100;
}
handler.obtainMessage(DownloadActivity.MSG_SET_PROGRESS, i, 0).sendToTarget();
out.write(calcCRC8(data));
}
if (cancelled) {
throw new Exception();
}
// 下载完成后启动固件
out.write(0xab);
complete = true;
handler.obtainMessage(DownloadActivity.MSG_ALERT, "固件下载完毕").sendToTarget();
} else if (conn.getServiceState() == DeviceConnection.SERVICE_NOT_FOUND) {
handler.obtainMessage(DownloadActivity.MSG_ALERT, "此蓝牙设备不支持串口服务").sendToTarget();
} else {
handler.obtainMessage(DownloadActivity.MSG_ALERT, "连接蓝牙设备失败").sendToTarget();
}
} catch (IOException e) {
if (handler != null) {
handler.obtainMessage(DownloadActivity.MSG_ALERT, "下载固件时发生错误").sendToTarget();
}
} catch (Exception e) {
if (handler != null) {
if (cancelled) {
handler.sendEmptyMessage(DownloadActivity.MSG_THREAD_END);
} else {
handler.obtainMessage(DownloadActivity.MSG_ALERT, e.getMessage()).sendToTarget();
}
}
}
}
}
app/src/main/java/com/oct1158/uartdfu/HexFile.java:
package com.oct1158.uartdfu;
import org.jetbrains.annotations.NotNull;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.Arrays;
public class HexFile {
public static final int ERR_NONE = 0;
public static final int ERR_NOT_FOUND = -1;
public static final int ERR_READ = -2;
public static final int ERR_CONTENT = -3;
public static final int ERR_CHECKSUM = -4;
public static final int ERR_MEMORY = -5;
private byte[] data;
private int address;
private int entry;
private int error;
private int size;
public HexFile(String filename) {
try (
FileReader fr = new FileReader(filename);
BufferedReader br = new BufferedReader(fr)
) {
int addr;
int baseAddr = 0;
int capacity = 0;
int endAddr = 0;
int started = 0;
String line;
while ((line = br.readLine()) != null) {
// 每行必须以冒号开头
if (line.isEmpty() || line.charAt(0) != ':') {
continue;
}
// 读取本行所有字节
int n = (line.length() - 1) / 2; // 本行字节数
if (n < 5) {
error = ERR_CONTENT;
break; // 每行至少5个字节
}
byte[] lineData = new byte[n];
for (int i = 0; i < n; i++) {
for (int j = 1; j <= 2; j++) {
char ch = line.charAt(2 * i + j);
if (ch >= '0' && ch <= '9') {
ch -= '0';
} else if (ch >= 'A' && ch <= 'F') {
ch = (char)(ch - 'A' + 10);
} else if (ch >= 'a' && ch <= 'f') {
ch = (char)(ch - 'a' + 10);
}
if (j == 1) {
lineData[i] = (byte)(ch << 4);
} else if (j == 2) {
lineData[i] |= ch & 15;
}
}
}
// 检查校验和
byte checksum = 0;
for (int i = 0; i < n; i++) {
checksum += lineData[i];
}
if (checksum != 0) {
error = ERR_CHECKSUM; // 校验不通过
break;
}
// 处理数据
switch (lineData[3]) {
case 0:
// 程序内容
addr = baseAddr | ((lineData[1] << 8) & 0xff00) | (lineData[2] & 0xff);
if (started == 0) {
started = 1;
address = addr;
}
endAddr = addr + (lineData[0] & 0xff);
size = endAddr - address;
if (size <= 0) {
// 出现错误
error = ERR_CONTENT;
break;
} else if (size > capacity) {
// 扩大缓冲区
while (size > capacity) {
capacity = capacity * 2 + 1024;
}
if (data == null) {
data = new byte[capacity];
} else {
data = Arrays.copyOf(data, capacity);
}
}
System.arraycopy(lineData, 4, data, addr - address, lineData[0] & 0xff);
break;
case 1:
// 程序结束
started = 2;
size = endAddr - address;
break;
case 4:
// 程序地址前16位
baseAddr = (lineData[4] << 24) | ((lineData[5] << 16) & 0xff0000);
break;
case 5:
// 程序入口地址
entry = (lineData[4] << 24) | ((lineData[5] << 16) & 0xff0000) | ((lineData[6] << 8) & 0xff00) | (lineData[7] & 0xff);
break;
}
if (error != ERR_NONE) {
break; // switch语句块里面有错误
}
}
if (started == 0) {
error = ERR_CONTENT;
}
} catch (FileNotFoundException e) {
error = ERR_NOT_FOUND;
} catch (IOException e) {
error = ERR_READ;
} catch (OutOfMemoryError e) {
// br.readLine()读取很长的一行, 有可能会内存不足
// 扩展缓冲区时, 执行new byte[]或Arrays.copyOf时, 也有可能会内存不足
error = ERR_MEMORY;
}
}
public byte[] getData() {
return Arrays.copyOfRange(data, 0, size); // 不能直接返回data, 因为data.length>=size
}
public byte[] getData(int offset, int size) {
return Arrays.copyOfRange(data, offset, offset + size);
}
public int getEndAddress() {
if (size >= 1) {
return address + size - 1;
} else {
return address;
}
}
public int getEntry() {
return entry;
}
public int getError() {
return error;
}
public String getErrorMessage() {
switch (error) {
case HexFile.ERR_NOT_FOUND:
return "文件不存在";
case HexFile.ERR_READ:
return "读取文件失败";
case HexFile.ERR_CONTENT:
case HexFile.ERR_CHECKSUM:
return "文件内容有误";
case HexFile.ERR_MEMORY:
return "系统内存不足";
default:
return null;
}
}
public int getSize() {
return size;
}
public int getStartAddress() {
return address;
}
@NotNull
public static String toHexAddress(int addr) {
return String.format("0x%08x", addr);
}
@NotNull
public static String toKBSize(int size) {
return String.format("%.2fKB", size / 1024.0);
}
}