蓝牙串口无线烧写STM32程序的安卓APP,版本V1.0,目前仅支持CC2541蓝牙模块。安卓9和10系统下测试没问题。
开发工具:Android Studio 3.5.2 下载地址:百度网盘 请输入提取码(提取码:mzf5)
实现程序内容发送的核心代码为DownloadThread类的run方法。
安卓Java采用的是大端序,而STM32单片机C语言采用的是小端序,在Java中由Integer.reverseBytes方法实现大小端序的相互转换。

android 串口检测工具 安卓串口工具_bluetooth

【主要代码】

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);
    }
}