Android DFU升级

  • DFU,全称是Device Firmware Upgrade

做蓝牙设备开发的,几乎都要遇到固件升级,我目前项目用的是Nordic的方案,所以就以此为基础,记录一下。
升级流程:或者设备的版本 -> 去服务器查看是否有更新包 -> 下载更新包 -> 校验升级 -> 指定DFU模式的名字 -> 进入DFU -> 传升级包 -> 完成后会自动重启
Nordic Github主页
Nordic 官网
如果你家的蓝牙芯片用的是Nordic方案,建议你下载他们的调试工具,叫
nRF Connect,去各个平台找,都有,可以帮你更好的理解。


蓝牙设备升级使用的UUID服务跟平时通信用的不一样,可以用工具查看
DFU Service 的UUID:
0000fe59-0000-1000-8000-00805f9b34fb
特征值UUID:
8ec90003-f315-4F60-9FB8-838830daea50
先开启 indicate ,如果对设备有重命名的要求,可以先把名字发给设备,没这个需求的就不用管,DFU模式默认名字是 DfuTarg 然后再发送进入DFU的指令,设备就会重启,自动到DFU模式,再把升级包发送过去,接受回调完成升级即可。

DFU UUID.jpg


1.build.gradle加入远程依赖

implementation 'no.nordicsemi.android:dfu:1.9.0'
//如果你是用Jetpack
implementation 'no.nordicsemi.android:dfu:1.8.1'

2.创建一个NotificationActivity

class NotificationActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        if (isTaskRoot) {
            // Start the app before finishing
            //DeviceManageActivity是升级的activity
            val intent = Intent(this, DeviceManageActivity::class.java)
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
            intent.putExtras(getIntent().extras!!) // copy all extras
            startActivity(intent)
        }

        finish()
    }
}

3.创建一个DfuService,继承与DfuBaseService

class DfuService : DfuBaseService() {
    override fun getNotificationTarget(): Class<out Activity> {
        return NotificationActivity::class.java
    }

    override fun onBind(intent: Intent?): IBinder? {
        return super.onBind(intent)
    }
}

4.我写了一个fragmentDialog,将升级内容都放在这,使用了FastBle,大家也可以用自己写的蓝牙库,或者Nordic也有。
FastBle
中文文档
Android BLE开发详解和FastBle源码解析


object Constant {
    //进入duf,uuid
    val dfuUuid = "8ec90003-f315-4F60-9FB8-838830daea50"
    val dfuServiceUuid = "0000fe59-0000-1000-8000-00805f9b34fb"

    //进入DFU模式
    val dfuEnterBootLoader: Byte = 0x01
    //设置DFU模式的名字
    val dfuSetName: Byte = 0x02

    //dfu模式的名字
    val dfuName = "BaoDfuTarg"
}
/**
 * 固件升级dialog
 */
class FirmwareUpdateDialogFragment : BaseDialogFragment() {

    /**
     * 升级监听、回调
     */
    private val dfuProgressListener = object : DfuProgressListener {
        override fun onProgressChanged(deviceAddress: String?, percent: Int, speed: Float, avgSpeed: Float, currentPart: Int, partsTotal: Int) {
            //升级进度
            setProgress(percent)
            LogUtils.d("DFU onProgressChanged")
        }

        override fun onDeviceDisconnecting(deviceAddress: String?) {
            LogUtils.d("DFU onDeviceDisconnecting")
        }

        override fun onDeviceDisconnected(deviceAddress: String?) {
            LogUtils.d("DFU onDeviceDisconnected")
        }

        override fun onDeviceConnected(deviceAddress: String?) {
            LogUtils.d("DFU onDeviceConnected")
        }

        override fun onDfuProcessStarting(deviceAddress: String?) {
            LogUtils.d("DFU onDfuProcessStarting")
        }

        override fun onDfuAborted(deviceAddress: String?) {
            //升级流产
            LogUtils.d("DFU onDfuAborted")
            dfuError()
        }

        override fun onEnablingDfuMode(deviceAddress: String?) {
            LogUtils.d("DFU onEnablingDfuMode")
        }

        override fun onDfuCompleted(deviceAddress: String?) {
            //升级完成
            LogUtils.d("DFU onDfuCompleted")
            onDfuResultListener?.let {
                it.onSuccess()
            }
            dfuFinish()
        }

        override fun onFirmwareValidating(deviceAddress: String?) {
            LogUtils.d("DFU onFirmwareValidating")
        }

        override fun onDfuProcessStarted(deviceAddress: String?) {
            LogUtils.d("DFU onDfuProcessStarted")
        }

        override fun onError(deviceAddress: String?, error: Int, errorType: Int, message: String?) {
            //失败
            LogUtils.d("DFU onError")
            dfuError()
        }

        override fun onDeviceConnecting(deviceAddress: String?) {
            LogUtils.d("DFU onDeviceConnecting")
        }
    }

    private var onDfuResultListener: OnDfuResultListener? = null
    private lateinit var normalDialogFragment: NormalDialogFragment
    private lateinit var progressBar: ProgressBar

    override fun layoutResource() = R.layout.firmware_update_dialog
    private lateinit var mac: String

    companion object {
        fun newInstance(mac: String) = FirmwareUpdateDialogFragment().apply {
            arguments = Bundle().apply {
                //用mac地址指定你要升级的设备
                putString("mac", mac)
            }
        }
    }


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setStyle(STYLE_NORMAL, R.style.HeightDialogStyle)
        arguments?.let {
            mac = it.getString("mac")
            if (mac.isNullOrEmpty()) {
                dismissDialog()
            }
        }
        context?.let {
            DfuServiceListenerHelper.registerProgressListener(it, dfuProgressListener)
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        context?.let {
            DfuServiceListenerHelper.unregisterProgressListener(it, dfuProgressListener)
        }
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        normalDialogFragment = NormalDialogFragment
                .newInstance()
                .setMessage("升级完成")
                .setNegativeVisible(false)
                .setPositive(getString(R.string.i_know))
                .setOnClickListener(object : NormalDialogFragment.OnClickListener {
                    override fun onPositive(positive: String) {
                    }

                    override fun onNegative(negative: String) {
                    }
                })

        var btnUpdate = root.findViewById<TextView>(R.id.btnUpdate)
        btnUpdate.setOnClickListener {
          
            root.findViewById<View>(R.id.btnClose).visibility = View.INVISIBLE
            btnUpdate.isEnabled = false
            btnUpdate.setText(R.string.updating)
            connectDevice()
        }

        root.findViewById<View>(R.id.btnClose).setOnClickListener {
            dismissDialog()
        }

        progressBar = root.findViewById(R.id.progressBar)

    }

    fun setProgress(progress: Int) {
        progressBar.progress = progress
    }

    /**
     * 升级完成
     */
    fun dfuFinish() {
        normalDialogFragment.showDialog(fragmentManager!!)
        dismissDialog()
    }

    /**
     * 升级失败
     */
    private fun dfuError() {
        onDfuResultListener?.let {
            it.onError()
        }
        dismissDialog()
    }

    /**
     * 将DFU的蓝牙模块跟普通的分离
     * 重新连接回设备
     */
    private fun connectDevice() {
        Thread.sleep(500)
        var bleScanRuleConfig = BleScanRuleConfig.Builder()
                .setDeviceMac(mac)
                .build()
        BleManager.getInstance().initScanRule(bleScanRuleConfig)
        BleManager.getInstance().scanAndConnect(object : BleScanAndConnectCallback() {
            override fun onStartConnect() {
            }

            override fun onScanStarted(success: Boolean) {
            }

            override fun onDisConnected(isActiveDisConnected: Boolean, device: BleDevice?, gatt: BluetoothGatt?, status: Int) {
            }

            override fun onConnectSuccess(bleDevice: BleDevice?, gatt: BluetoothGatt?, status: Int) {
                indicate(bleDevice!!)
            }

            override fun onScanFinished(scanResult: BleDevice?) {
                scanResult
            }

            override fun onConnectFail(bleDevice: BleDevice?, exception: BleException?) {
            }

            override fun onScanning(bleDevice: BleDevice?) {
            }
        })
    }

    /**
     * dfu indicate
     */
    private fun indicate(bleDevice: BleDevice) {
        BleManager.getInstance().indicate(bleDevice
                , Constant.dfuServiceUuid
                , Constant.dfuUuid
                , object : BleIndicateCallback() {
            override fun onCharacteristicChanged(data: ByteArray?) {
            }

            override fun onIndicateSuccess() {
                writeDfuName(bleDevice)
            }

            override fun onIndicateFailure(exception: BleException?) {
            }
        })
    }

    /**
     * 进入dfu前,是可以对设备命名的,设备重启后,会以这个名字广播出来
     * 指定dfu名字,02+名字长度+名字
     */
    private fun writeDfuName(bleDevice: BleDevice) {
        BleManager.getInstance().write(bleDevice
                , Constant.dfuServiceUuid
                , Constant.dfuUuid
                , byteArrayOf(Constant.dfuSetName, Constant.dfuName.length.toByte()) + Constant.dfuName.toByteArray()
                , object : BleWriteCallback() {
            override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray?) {
                //进入dfu模式指令
                writeDfu(bleDevice)
            }

            override fun onWriteFailure(exception: BleException?) {
                //写入失败
                dfuError()
            }
        })
    }

    /**
     * 发送指令进入DFU
     */
    private fun writeDfu(bleDevice: BleDevice) {
        BleManager.getInstance().write(bleDevice
                , Constant.dfuServiceUuid
                , Constant.dfuUuid
                , byteArrayOf(Constant.dfuEnterBootLoader)
                , object : BleWriteCallback() {
            override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray?) {
                //通知设备进入DFU模式
                scanDfu()

            }

            override fun onWriteFailure(exception: BleException?) {
                //写入失败
                dfuError()
            }
        })
    }

    /**
     * 扫描DFU模式的机子
     */
    private fun scanDfu() {
        BleManager.getInstance().disconnectAllDevice()
        //设置搜索对应名字的设备
        var bleScanRuleConfig = BleScanRuleConfig.Builder()
                .setDeviceName(true, Constant.dfuName)
                .build()
        BleManager.getInstance().initScanRule(bleScanRuleConfig)
        //开始搜索
        BleManager.getInstance().scanAndConnect(object : BleScanAndConnectCallback() {
            override fun onStartConnect() {
            }

            override fun onScanStarted(success: Boolean) {
            }

            override fun onDisConnected(isActiveDisConnected: Boolean, device: BleDevice?, gatt: BluetoothGatt?, status: Int) {
            }

            override fun onConnectSuccess(bleDevice: BleDevice?, gatt: BluetoothGatt?, status: Int) {
                if (bleDevice?.name == Constant.dfuName) {
                    Thread.sleep(500)
                    //DFU升级包路径
                    val zip = Environment.getExternalStorageDirectory().absolutePath + File.separator + Environment.DIRECTORY_DOWNLOADS + File.separator + "test.zip"
                    var starter = DfuServiceInitiator(bleDevice.mac)
                            .setDeviceName(bleDevice.name)
                            .setKeepBond(false)
                            .setDisableNotification(true)
                            .setPacketsReceiptNotificationsEnabled(false)
                            .setPacketsReceiptNotificationsValue(10)
                            .setUnsafeExperimentalButtonlessServiceInSecureDfuEnabled(true)
                            .setZip(zip)
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                        //Android O,通知栏问题
                        DfuServiceInitiator.createDfuNotificationChannel(context!!)
                    }
                    context?.let {
                        starter.start(it, DfuService::class.java)
                    }
                }
            }

            override fun onScanFinished(scanResult: BleDevice?) {
                //搜索不到,失败
                if (scanResult == null) {
                    dfuError()
                }
            }

            override fun onConnectFail(bleDevice: BleDevice?, exception: BleException?) {
                //连接失败
                dfuError()
            }

            override fun onScanning(bleDevice: BleDevice?) {
            }
        })
    }

    /**
     * 升级结果
     */
    interface OnDfuResultListener {
        fun onSuccess()

        fun onError()
    }

    fun setOnDfuResultListener(listener: OnDfuResultListener) {
        this.onDfuResultListener = listener
    }
}

使用

 //固件升级?
dataBinding.clFirmwareUpdate.setOnClickListener {
    val firmwareDialog = FirmwareUpdateDialogFragment.newInstance(viewModel.mg03.mac)
    firmwareDialog.setOnDfuResultListener(object : FirmwareUpdateDialogFragment.OnDfuResultListener {
        override fun onSuccess() {
            //升级完成,重新连接
            connectDevice()
        }

        override fun onError() {
            //升级失败
            connectDevice()
            ToastUtils.shortToast(R.string.update_error)
        }

    })
    firmwareDialog.showDialog(fragmentManager!!)
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352