android 蓝牙设备 ota dfu升级实录

image.png

    最近做了一个小功能,由于没有需求,只有一个一代的app services功能实现进行提示。由于更换了外包厂商,所以在升级版本上需要自行研发。然而一直从事底层开发的我,一脸懵逼,后来验证了,这根本就是n脸懵逼。
    首先下载dex2jar对apk进行反编译,然后用jd-gui打开。经理说可以参考这个进行开发,我当时一看这不是很easy么,源码都有了,再编译一下就成。然而实在是太年轻,当时不明白这些变量的名字为什么这么奇怪,后来才知道,apk经过混淆,变量名都变了的。首先摸清楚业务逻辑就花了大力气,因为混淆过的app反解出来的代码,逻辑不完全一样,之可以知道大概做了些什么事,至于逻辑,需要重新组织。
    至于需要使用的第三方dfu library也是不甚了解,这里将上面反解出来的classes-dex2jar.jar作为jar包,放入到android studio工程中的app/libs即可以完成dfu library库的导入。这里就可以使用第三方的接口进行升级操作了。首先新建一个android项目,然后如上导入jar包,然后开始创建reciever。

package com.包名;

import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.util.Log;

import java.lang.reflect.Method;
import java.io.File;

public class ControllerOTARec extends BroadcastReceiver {
    private static final String TAG = "ControllerRec";
    private static final String ACTION_READY_FOR_OTA = "com.action.readyForOTA";
    private static final String DEVICE_NAME = "DEVICE";
    public static final String RETRY_OTA_UPGRADE = "retry.ota.upgrade";
    private static final String BT_CONNECTED = "android.bluetooth.device.action.ACL_CONNECTED";
    private static final String GET_HAND_VERSION ="com.test.gethandversion";
    private  static final File otaFile = new File(ControllerOTAService.mFile);
    public static boolean mHaveDisable = false;
    public static boolean mHaveStart = false;
    private int mOldVersion;
    private int mNewVersion;
    private Method getStringMethod = null;
    public  static BluetoothDevice mDevice = null;


    @Override
    public void onReceive(final Context context, final Intent intent0)
    {
        String actionStr =  intent0.getAction();

        if (BT_CONNECTED.equals(actionStr))
        {
            mDevice = (BluetoothDevice)intent0.getParcelableExtra("android.bluetooth.device.extra.DEVICE");
        } else if(ACTION_READY_FOR_OTA.equals(actionStr)){
            mNewVersion = getStringMethod("ro.build.controller.version", null);
            int mOldVersion = Integer.parseInt(getStringMethod("sys.hand.appVersion","1"));

            //catch if target version lower than present
            if ((mNewVersion <= mOldVersion) || (mOldVersion == 0)) {
                Log.d(TAG, "controller version is very new,not ota!");
                return;
            }

            //catch if zipfile is not exist
            if (!otaFile.exists()) {
                Log.d(TAG, "ota file not exists");
                return;
            }

            //begin to do the things
            if (mDevice != null) {
                final String deviceName = mDevice.getName();
                final String deviceAddr = mDevice.getAddress();
                int deviceBond = mDevice.getBondState();
                Log.d(TAG, "deviceName:" + deviceName + ";and bond state is  " + deviceBond + "; and device address is " + deviceAddr);

                //通知蓝牙状态机马上进入ota模式,停止
                if(!mHaveDisable) {
                    Log.d(TAG, "tell the bluetooth machine to stop for ota");
                    Intent intent = new Intent("bluetooth_ota_update");
                    intent.putExtra("start_bluetooth_state", false);
                    context.sendBroadcast(intent);

                    //delay 500ms for bt to stop
                    try {
                        Thread.sleep(500);
                    } catch (Exception e){
                        Log.d(TAG, "Sleep error");
                    }
                }


                if (!mHaveStart) {
                    removeBondStatus(mDevice);//这是清楚蓝牙配对,这是遇到的最大的一个问题
                    //储存设备地址和设备名信息
                    Log.d(TAG, "mHaveStart = true");
                    ControllerOTARec.mHaveStart = true;
                    SharedPreferences.Editor editor = context.getSharedPreferences("Controller_OTA", 0).edit();
                    editor.putString("deviceName", deviceName);
                    editor.putString("deviceAddress", deviceAddr);
                    editor.commit();

                    //告诉cs开始进入ota模式
                    Log.d(TAG,"will tell the controller service to stop for ota");
                    Intent intent1 = new Intent("com.startsignal");
                    context.sendBroadcast(intent1);

                    //准备工作完成,启动服务
                    Log.d(TAG, "start ota:" + mDevice.getAddress());
                    Intent intent2 = new Intent(context, ControllerOTAService.class);
                    intent2.putExtra("deviceName", deviceName);
                    intent2.putExtra("deviceAddress", deviceAddr);
                    context.startService(intent2);
                }
            }
        } else if(RETRY_OTA_UPGRADE.equals(actionStr)){
            Log.d(TAG, "wille retry for hand devices ota");
            Intent intent = new Intent(context, ControllerOTAService.class);
            String add = context.getSharedPreferences("Controller_OTA",0).getString("deviceAddress","default");
            intent.putExtra("deviceName",DEVICE_NAME);
            intent.putExtra("deviceAddress",add);
            context.startService(intent);
        }
    }
    //反射的方式访问系统属性
    public String getStringMethod(final String key, final String def) {
        try {
            if (getStringMethod == null) {
                getStringMethod = Class.forName("android.os.SystemProperties").getMethod("get", String.class, String.class);
            }
            return ((String) getStringMethod.invoke(null, key, def)).toString();
        } catch (Exception e) {
            return def;
        }
    }
    //反射的方式访问蓝牙设备的隐藏接口removebond
    public void removeBondStatus(BluetoothDevice btDevices){
        boolean result = false;
        try {
            final Method removeBondStatus = btDevices.getClass().getMethod("removeBond");
            if (removeBondStatus != null) {
                result = (Boolean) removeBondStatus.invoke(btDevices);
                Log.w(TAG, "removeBondStatus result is " + result);

                while (btDevices.getBondState() != BluetoothDevice.BOND_NONE){
                    Thread.sleep(20);
                }
            }

        } catch (final Exception e) {
            Log.w(TAG, "An exception occurred while removing bond information", e);
        }
    }
}

然后创建服务

package com.包名;

import android.app.ProgressDialog;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;

import java.io.File;

import no.nordicsemi.android.dfu.DfuProgressListener;
import no.nordicsemi.android.dfu.DfuServiceInitiator;
import no.nordicsemi.android.dfu.DfuServiceListenerHelper;

public class ControllerOTAService extends Service {
    //定义安装包路径
    public static final String ZIP_FILE_PATH = "/sdcard/Controller/";
    //获取安装包文件名
    public static final String mFile = getmPathFile();
    //进度dialog
    private ProgressDialog mDialog;
    //通过路径找文件名
    public  static String getmPathFile(){
        String otapackagepath = null;
        File file = new File(ZIP_FILE_PATH);
        File[] array = file.listFiles();
        if(array[0].isFile()){
            otapackagepath = ZIP_FILE_PATH +array[0].getName();
            Log.d("ControllerOTAService", "file path is :" + otapackagepath);
        } else{
            Log.d("ControllerOTAService", "can not find the zip file");
        }
        return  otapackagepath;
    }

    public ControllerOTAService() {
    }
    //注销监听,停止服务
    private void stopService()
    {
        DfuServiceListenerHelper.unregisterProgressListener(this, mDfuProgressListener);//取消监听升级回调
        stopSelf();
    }
   //启动升级流程
    private void beginToUpdate(String devname, String addr)
    {
        Log.d("ControllerOTAService", "begin to update hand device by DFU,create dialog");
        createDialog();
        if (this.mDialog != null)
            this.mDialog.show();

        Log.d("ControllerOTAService", "devices name is " + devname +", address is " + addr + ", zip file path is " + mFile);
        final DfuServiceInitiator dfuservice = new DfuServiceInitiator(addr)
                .setDeviceName(devname)
                .setKeepBond(true);//升级完成后保持连接
        dfuservice.setZip(null, mFile);
        dfuservice.start(this, DfuService.class);

        DfuServiceListenerHelper.registerProgressListener(this, mDfuProgressListener);//注册监听

        Log.d("ControllerOTAService", "dismiss dialog");
        //取消dialog
        new Thread(new Runnable() {
            @Override
            public void run() {
                try{
                    Thread.sleep(5000);
                    ControllerOTAService.this.mDialog.dismiss();
                } catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
        }).start();

    }
   //创建dialog
    private void createDialog()
    {
        Log.d("ControllerOTAService", "create dialog");
        this.mDialog = new ProgressDialog(this);
        this.mDialog.setMax(100);
        this.mDialog.setProgress(0);
        this.mDialog.setProgressStyle(1);
        this.mDialog.setTitle("hand ota");
        this.mDialog.setMessage("start update hand device,please use head mode");
        this.mDialog.setIndeterminate(false);
        this.mDialog.setCancelable(true);
        this.mDialog.getWindow().setType(2003);
    }
   //startservice会调到这里
    public int onStartCommand(Intent intent, int int1, int int2)
    {
        String devname = intent.getStringExtra("deviceName");
        String addr = intent.getStringExtra("deviceAddress");
        Log.d("ControllerOTAService", "deviceName:" + devname + " deviceAddress:" + addr);
        beginToUpdate(devname, addr);
        return Service.START_NOT_STICKY;
    }

    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        throw new UnsupportedOperationException("Not yet implemented");
    }
    //新建dfu升级流程监听(新建后自动弹出接口函数)
    private  final DfuProgressListener mDfuProgressListener = new DfuProgressListener() {
        //device connecting
        @Override
        public void onDeviceConnecting(String deviceAddress) {
            Log.d("ControllerOTAService", "onDeviceConnecting");
        }
        //devices begin to connect
        @Override
        public void onDeviceConnected(String deviceAddress) {
            Log.d("ControllerOTAService", "onDeviceConnected");
        }
        //before ota process start
        @Override
        public void onDfuProcessStarting(String deviceAddress) {
            Log.d("ControllerOTAService", "onDfuProcessStarting");
        }

        @Override
        public void onDfuProcessStarted(String deviceAddress) {
            Log.d("ControllerOTAService", "onDfuProcessStarted");
        }

        @Override
        public void onEnablingDfuMode(String deviceAddress) {
            Log.d("ControllerOTAService", "onEnablingDfuMode");
        }

        @Override
        public void onProgressChanged(String deviceAddress, int percent, float speed, float avgSpeed, int currentPart, int partsTotal) {
            Log.d("ControllerOTAService", "onProgressChanged");
        }

        @Override
        public void onFirmwareValidating(String deviceAddress) {
            Log.d("ControllerOTAService", "onFirmwareValidating");
        }

        @Override
        public void onDeviceDisconnecting(String deviceAddress) {
            Log.d("ControllerOTAService", "onDeviceDisconnecting");
        }

        @Override
        public void onDeviceDisconnected(String deviceAddress) {
            Log.d("ControllerOTAService", "onDeviceDisconnected");
        }
        //完成升级会调入到这里
        @Override
        public void onDfuCompleted(String deviceAddress) {
            Log.d("ControllerOTAService", "onDfuCompleted");
            //发出停止广播
            Intent intent = new Intent("com.handota.stop");
            ControllerOTAService.this.sendBroadcast(intent);
            //恢复状态机
            Intent intent1 = new Intent("bluetooth_ota_update");
            intent1.putExtra("start_bluetooth_state", true);
            ControllerOTAService.this.sendBroadcast(intent1);
            //是否启动ota状态改为false
            ControllerOTARec.mHaveStart = false;
            
            //tell app the result
            Intent intent2 = new Intent("hand_device_ota_process");
            intent2.putExtra("handOTAProcess",1);
            ControllerOTAService.this.sendBroadcast(intent2);

            //DFU completed will delete the zip file
            Log.d("ControllerOTAService", "begin to delete the update zipfile");
            clearFile(ZIP_FILE_PATH);

            Log.d("ControllerOTAService", "<<

    这里还要创建dfuservices类

package com.包名;

import android.app.Activity;
import no.nordicsemi.android.dfu.DfuBaseService;

public class DfuService extends DfuBaseService
{
    protected Class getNotificationTarget()
    {
        return NotificationActivity.class;
    }

    protected boolean isDebug()
    {
        return true;
    }
}

    创建NotificationActivity通知类

package com.包名;

import android.os.Bundle;
import android.app.Activity;

public class NotificationActivity extends Activity {

    @Override
    protected void onCreate(Bundle bundle)
    {
        super.onCreate(bundle);
        finish();
    }
}

    配置xml




    
    
    
    
    
    
    

    
        
            
                

                
            
        

        
        

        
            
                
                
                
            
        
    


    配合蓝牙状态机升级流程大致如下


image.png

    遇到的问题:
    1.不同版本的官方nordic软件,升级方式也不一样,1.61.3版本连接状态下可以直接进行升级;2.6.0版本必须设备通过硬件进入ota模式才能正常升级,同时,高版本的dfu代码在18年7月已经对O代码做了适应,同时到O有些接口发生了变化,得确认是否在自己项目里面支持,不对应的话,需要下载对应的dfu版本的三方库;
    2.读取设备版本这里遇到过一些问题,由于代码设计问题,设备当前版本在目前项目里面经常被重置为1,这里采取临时的系统属性来储存正确读取的当前版本;
    3.由于一开始使用的是反编译的apk做jar包,有些库函数并不能被反解出来,导致内部逻辑无法获知,后来在github上下载了源码,就可以看到了;
    4.遇到一个读取dfu版本失败的问题,追Src read.p_value ptr is NULL发现深陷蓝牙协议栈中的调用无法自拔,找不到根本原因,追Reading DFU version number发现是dfuservices要发起一此读取dfu版本信息的请求,然后gattserver收到这个请教,通过jni,向蓝牙协议栈发送了这个请求,然后返回的结果不对,status为1,正确的情况status为0,一开始蓝牙这一块的东西什么都不懂的时候有点摸不清头脑。为什么这里要去读这个,而且这个读取操作在升级过程中有两到三次。整个升级流程也是一头雾水。后来慢慢的梳理log,发现在gatt server向手柄发送了进入ota模式的命令后,然后再去读取手柄相关属性就会失败。
    这里涉及到一个隐藏任务,设备在ota模式和非ota模式,属性值会发生变化,在手柄进入ota模式后,主机设备的蓝牙服务还会进行一次扫描,然后才会开始发送安装包进行升级的操作。然而新产品上为什么协议栈没有进行扫描,这一点需要一定蓝牙知识进行调查可能会更快解决。这里的临时做法,就是先进行设备配对信息清除,清楚完之后,设备就会进行扫描(可能跟产品里面的状态机有关系,在有配对信息的情况下,就不会进行扫描。)。即是这里的removeBondStatus(mDevice);这一块困住了很长时间,在宇神的一次操作下成功绕出去了,就得到了这个workaround。

DfuImpl : Reading DFU version number...
WCNSS_FILTER: ibs_recv_ibs_cmd: Received IBS_WAKE_IND: 0xFD
WCNSS_FILTER: ibs_recv_ibs_cmd: Writing IBS_WAKE_ACK
WCNSS_FILTER: do_write: IBS write: fc
bt_btif : btapp_gattc_req_data :Src read.p_value ptr is NULL for event  0x3
BluetoothGatt: onCharacteristicRead() - Device=D0:F0:63:61:D4:B1 handle=28 Status=1
DfuImpl : Characteristic read error: 1
DfuBaseService: Unable to read version number (error 1)
DfuBaseService: Disconnecting from the device...
BluetoothGatt: cancelOpen() - device: D0:F0:63:61:D4:B1
BtGatt.GattService: clientDisconnect() - address=D0:F0:63:61:D4:B1, connId=8
BtGatt.GattService: onDisconnected() - clientIf=8, connId=8, address=D0:F0:63:61:D4:B1
BluetoothGatt: onClientConnectionState() - status=0 clientIf=8 device=D0:F0:63:61:D4:B1
DfuBaseService: Disconnected from GATT server
DfuBaseService: Cleaning up...
BluetoothGatt: close()
BluetoothGatt: unregisterApp() - mClientIf=8

    5.apk实现后,怎样将android studio集成到android source项目当中去也是因为不太熟悉这块有太多的问题,首先是sdk版本过高,将apk放入android项目失败的问题。这里去android studio中手动从O降级到N,第三方库的降级方法就是一个个去解决编译错误,找到对应的地版本对象进行替换。这里的25都是由27转换过来的。

apply plugin: 'com.android.application'

android {
    signingConfigs {
        config {
            keyAlias 'controllerota'
            keyPassword '123456'
            storeFile file('/home/edward/bin/android-studio/keystore/key.jks')
            storePassword '123456'
        }
    }
    compileSdkVersion 25
    defaultConfig {
        applicationId "com.qiyi.controllerota1"
        minSdkVersion 23
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation 'com.android.support:appcompat-v7:25.1.1'
    implementation 'com.android.support.constraint:constraint-layout:1.1.2'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
    implementation files('libs/DFUlibrary.jar')
}

    6.关于系统ro属性的修改方法,在前一篇中记录了,之修改buildinfo.sh似乎是不奏效的。
    最后一点忠告:就是尽量做需求清晰,设计明确的工作,降低跟同事工作的耦合度---------------虽然空白做法也挺锻炼人。
    完全知识分享,谢谢支持

你可能感兴趣的:(android 蓝牙设备 ota dfu升级实录)