Android客户端访问NRF52x属性服务

本文参考 nRF Toolbox v2.7.3 源码 实现一个 android客户端与nrf52810设备通信的框架,并讨论了从原始框架演化出一种新的跨进程框架,具备高内聚低耦合的面向对象设计原则,提高框架的复用性。



开发环境准备

  1. IDE Android studio 3.5
  2. Android-nRF-Toolbox 2.7.3 源码
  3. 创建一个示例工程,我们只需要部分源码加以改造来实现一个 ble uart 通信框架
  4. nrf52810设备端工程内容参见我的文章 NRF52x属性服务示例



实现目标

系统通信时序图如下:
在这里插入图片描述
我们采用国际惯例:

  1. app通过 uart tx/rx 属性服务 发送"hello" 到 ble设备
  2. ble设备收到该字串后回应app “world”

在实际项目中,可以定义私有通信协议,完成app与ble设备更丰富的交互。



原UART客户端分析

由于我们只需要基于uart属性服务做通信扩展,所以对于Android-nRF-Toolbox源码,我们只分析UART相关源码,其他类型的属性服务不在本文讨论范围。
UART客户端类图如下:
在这里插入图片描述

  • 主要类说明
  1. UARTActivity / BleProfileServiceReadyActivity
    工作时与服务UARTService 绑定,负责发起蓝牙设备扫描与连接、指令编辑发送以及uart log展示等功能。
  2. UARTService / BleProfileService
    持有ble uart设备访问对象(UARTManager),是UARTManager对象与UARTActivity 交互的桥梁,注意 UARTService 与 UARTManager是 1对0/1 的关系。
  3. UARTManager/LoggableBleManager / BleManager
    通过蓝牙协议栈与ble设备的uart属性服务交互。

  • 主要接口说明
  1. UARTInterface
    uart tx属性写入接口,作为app发送指令的api。
  2. UARTManagerCallbacks / BleManagerCallbacks
    uart rx/tx 属性数据读入时的回调接口,作为ble设备回复数据api。


构建属性服务管理类

本节所述的属性服务内容参见 NRF52x属性服务示例
创建一个DevBleManager类,整合以下几方面内容:

  • 原UARTManager功能及触摸按键扩充属性访问
  • 原BatteryManager功能
  • 设备信息服务属性
    在这里插入图片描述
  1. 电池服务
	// Battery Service UUID.
    private final static UUID BATTERY_SERVICE_UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb");
    // Battery Level characteristic UUID.
    private final static UUID BATTERY_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb");
	private BluetoothGattCharacteristic mBatteryLevelCharacteristic;
	
    private DataReceivedCallback mBatteryLevelDataCallback = new BatteryLevelDataCallback() {
        @Override
        public void onBatteryLevelChanged(@NonNull final BluetoothDevice device, final int batteryLevel) {
            log(LogContract.Log.Level.APPLICATION, "Battery Level received: " + batteryLevel + "%");
            mBatteryLevel = batteryLevel;
            mCallbacks.onBatteryLevelChanged(device, batteryLevel);
        }

        @Override
        public void onInvalidDataReceived(@NonNull final BluetoothDevice device, final @NonNull Data data) {
            log(Log.WARN, "Invalid Battery Level data received: " + data);
        }
    };
	
	public void readBatteryLevelCharacteristic() {
        readCharacteristic(mBatteryLevelCharacteristic)
                .with(mBatteryLevelDataCallback)
                .fail((device, status) -> log(Log.WARN, "Battery Level characteristic not found"))
                .enqueue();
    }

    public void enableBatteryLevelCharacteristicNotifications() {
        // If the Battery Level characteristic is null, the request will be ignored
        setNotificationCallback(mBatteryLevelCharacteristic)
                .with(mBatteryLevelDataCallback);
        enableNotifications(mBatteryLevelCharacteristic)
                .done(device -> log(Log.INFO, "Battery Level notifications enabled"))
                .fail((device, status) -> log(Log.WARN, "Battery Level characteristic not found"))
                .enqueue();
    }

  1. 设备信息服务
	// device information Service UUID.
    private final static UUID DEVICE_INFORMATION_SERVICE_UUID = UUID.fromString("0000180A-0000-1000-8000-00805f9b34fb");
    // < Manufacturer Name String characteristic UUID.
    private final static UUID MANUFACTURER_NAME_STRING_CHARACTERISTIC_UUID = UUID.fromString("00002A29-0000-1000-8000-00805f9b34fb");
    //其他几个属性值 参考固件实现:[NRF52x属性服务示例](https://blog.csdn.net/qq_42237638/article/details/103247432)
    ...
    // < Software Revision String characteristic UUID.
    private final static UUID SOFTWARE_REVISION_STRING_CHARACTERISTIC_UUID = UUID.fromString("00002A28-0000-1000-8000-00805f9b34fb");

    private BluetoothGattCharacteristic mManufacturerCharacteristic, /*..., */ mSoftwareCharacteristic;
    private DevInfo mDevInfo = null;
    /*
     * 设备信息
     */
    public class DevInfo {
        public String Manufacturer;
        //other fields...
        public String Software;
    }


	public void readDevInformationCharacteristic() {
        if (mSerialNumberCharacteristic != null && mDevInfo == null)
            mDevInfo = new DevInfo();

        readCharacteristic(mManufacturerCharacteristic)
                .with((device, data) -> {
                    final String text = data.getStringValue(0);
                    mDevInfo.Manufacturer = text;
                })
                .fail((device, status) -> log(Log.WARN, "mManufacturerCharacteristic not found"))
                .enqueue();
        //其他几个属性值的访问
        ...
        //最后一个属性值
        readCharacteristic(mSoftwareCharacteristic)
                .with((device, data) -> {
                    final String text = data.getStringValue(0);
                    mDevInfo.Software = text;

                    //回调客户端监听接口
                    if (mDevInfo != null) {
                        mCallbacks.onDevInformationRead(device, mDevInfo.Manufacturer,
                                mDevInfo.Model, mDevInfo.SerialNumber, mDevInfo.Hardware,
                                mDevInfo.Firmware, mDevInfo.Software);
                    }
                })
                .fail((device, status) -> log(Log.WARN, "mSoftwareCharacteristic not found"))
                .enqueue();


    }

  1. 触摸按键属性访问
	/**
     * < The UUID of the TP Characteristic.
     */ //触摸
    private final static UUID UART_TP_CHARACTERISTIC_UUID = UUID.fromString("6E400005-B5A3-F393-E0A9-E50E24DCCA9E");
    private BluetoothGattCharacteristic mTPCharacteristic;    

  1. 扩展 BleManagerGattCallback 对象内容
 	/**
     * BluetoothGatt callbacks for connection/disconnection, service discovery,
     * receiving indication, etc.
     */
    private final BleManagerGattCallback mGattCallback = new BleManagerGattCallback() {

        @Override
        protected void initialize() {
            //设备信息属性访问
            readDevInformationCharacteristic();

            //uart service
            setNotificationCallback(mTXCharacteristic)
                    .with((device, data) -> {
                        final String text = data.getStringValue(0);
                        if (!TextUtils.isEmpty(text)) {
                            mCallbacks.onDataReceived(device, text.trim());
                        }
                    });
            requestMtu(260).enqueue();
            enableNotifications(mTXCharacteristic).enqueue();

			//添加的触摸按键属性
            if (mTPCharacteristic != null) {
                setNotificationCallback(mTPCharacteristic)
                        .with((device, data) -> {
                            final int event = data.getIntValue(Data.FORMAT_UINT8, 0);                            
                            mCallbacks.onKeyEvent(device, event);
                        });
                enableNotifications(mTPCharacteristic).enqueue();
            }
            
            //电池服务访问
            readBatteryLevelCharacteristic();
            enableBatteryLevelCharacteristicNotifications();
        }

        @Override
        public boolean isRequiredServiceSupported(@NonNull final BluetoothGatt gatt) {
            final BluetoothGattService service = gatt.getService(UART_SERVICE_UUID);
            if (service != null) {
                mRXCharacteristic = service.getCharacteristic(UART_RX_CHARACTERISTIC_UUID);
                mTXCharacteristic = service.getCharacteristic(UART_TX_CHARACTERISTIC_UUID);
                mTPCharacteristic = service.getCharacteristic(UART_TP_CHARACTERISTIC_UUID);
            }

            //other code...

            return mRXCharacteristic != null && mTXCharacteristic != null && (writeRequest || writeCommand);
        }

        @Override
        protected boolean isOptionalServiceSupported(@NonNull final BluetoothGatt gatt) {
            final BluetoothGattService service = gatt.getService(BATTERY_SERVICE_UUID);
            if (service != null) {
                mBatteryLevelCharacteristic = service.getCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID);
            }

            final BluetoothGattService service1 = gatt.getService(DEVICE_INFORMATION_SERVICE_UUID);
            if (service1 != null) {
                mManufacturerCharacteristic = service1.getCharacteristic(MANUFACTURER_NAME_STRING_CHARACTERISTIC_UUID);
                //...
                mSoftwareCharacteristic = service1.getCharacteristic(SOFTWARE_REVISION_STRING_CHARACTERISTIC_UUID);
            }

            return mBatteryLevelCharacteristic != null ||
                    //other char...
                    mSoftwareCharacteristic != null;
        }

        @Override
        protected void onDeviceDisconnected() {
            mRXCharacteristic = null;
            mTXCharacteristic = null;
            mTPCharacteristic = null;

            mBatteryLevelCharacteristic = null;
            mBatteryLevel = null;
            mUseLongWrite = true;

            mManufacturerCharacteristic = null;
            //other char...
            mSoftwareCharacteristic = null;
        }
    };
  1. 定义DevManagerCallbacks
    Manager类需要将 设备信息、电池电量、按键事件 等设备数据传递出去,所以需要扩展UARTManagerCallbacks接口:
    在这里插入图片描述
    其中UARTManagerCallbacks、BatteryLevelCallback使用官方源码,DevServiceCallbacks内容如下:
	/**
 	* device uart ext char  & device information service char
 	*/
	public interface DevServiceCallbacks {
		/**
		* 设备信息
		*/
    	void onDevInformationRead(BluetoothDevice device,
                              String manufacturer,
                              String model,
                              String serialNumber,
                              String hardware,
                              String firmware,
                              String software);

    	/**
     	* 按键事件
     	* @param device
     	* @param event
     	*/
    	void onKeyEvent(BluetoothDevice device, int event);

    	/**
     	* ScanResult found device 扫描结果
     	* @param device
     	*/
    	void onFoundDevice(BluetoothDevice device);
	}



构建核心框架类

上节已经实现了关键的设备管理访问接口DevBleManager类,本节主要描述如何构建一个支持本地/跨进程服务框架。
这个框架对DevBleManager集合进行管理并提供对外交互的接口。

  • 跨进程系统框架图:
    在这里插入图片描述
    本框架的设计目标是支持多app客户端与设备管理服务app统一交互,各app控制它自己关心的ble设备,完成丰富的互动。
    举个例子:
    app1 为小坦克控制端;
    app2 为小飞机控制端;
    app3 为小舰艇控制端;
    service app仅负责各app与ble设备之间的数据交换,不负责具体内容。是不是有一种打造海陆空三军指挥中心的感觉 : )

  • 框架核心类图:
    在这里插入图片描述

  1. 借助android的binder通信实现设备管理代理模式,DevManagerProxy 本地模式绑定 DevLocalBinder,远程模式绑定 IDevAidlInterface.Stub :
	//DevManagerService.java
	//本地binder
	private DevLocalBinder mLocalBinder = new DevLocalBinder();
	//远程binder
    private IDevAidlInterface.Stub mRemoteBinder = new IDevAidlInterface.Stub() {
    	//code...
    }
	
    @Override
    public IBinder onBind(final Intent intent) {
        if (intent != null) {
            mIsRemote = intent.getBooleanExtra(EXTRA_REMOTE_FLAG, false);
        }

        Log.w(TAG, "onBind: remote=" + mIsRemote);

        if (mIsRemote) {
            Log.w(TAG, "getBinder: mRemoteBinder=" + mRemoteBinder);
            return mRemoteBinder;
        } else {
            Log.w(TAG, "getBinder: mLocalBinder=" + mLocalBinder);
            return mLocalBinder;
        }
    }
	
	//DevManagerProxy.java
	/**     
     * @param ctx
     * @param isRemote 指定代理是否为远程代理
     * @return
     */
    public static DevManagerProxy getInstance(Context ctx, boolean isRemote) {
        if (mInstance == null) {
            mInstance = new DevManagerProxy(ctx, isRemote);
        }
        return mInstance;
    }
    
	public void bindService(Context ctx) {
        if (mBinding == true || mBinded == true) {
            Log.e(TAG, "bindService:mBinding="+mBinding+", mBinded="+mBinded);
            return;
        }

        mBinding = true;
        Intent intent;
        if (mIsRemote) {
            intent = new Intent();
            intent.setAction(this.getClass().getPackage().getName() + ".action.REMOTE_DEVMAN");
            intent.setPackage(this.getClass().getPackage().getName());
        } else {
            intent = new Intent(ctx, DevManagerService.class);
        }
		
        intent.putExtra(DevManagerService.EXTRA_REMOTE_FLAG, mIsRemote);
        ctx.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
    }	


	//app客户端
	//构建DevManagerProxy对象 指定为远程模式
	Context ctx = getApplicationContext();
	mDevManagerProxy = DevManagerProxy.getInstance(ctx, true);
	mDevManagerProxy.setManagerCallbacks(mDevManagerCallbacks);
	mDevManagerProxy.bindService(ctx);
  1. 取得binder对象后,通过binder对象的 DevLocalBinder.setCallbacks(本地)/ IDevAidlInterface.Stub.addCallbacks(远程)设置客户端回调接口:
	//DevManagerService.java
	//本地binder	
	public class DevLocalBinder extends LocalBinder implements UARTInterface {
       
        /**
         * 设置proxy local DevManagerCallbacks        
         * @param callbacks
         */
        public void setCallbacks(DevManagerCallbacks callbacks) {
            mLocalCallbacks = callbacks;
        }
    }
    
	//远程binder
    private IDevAidlInterface.Stub mRemoteBinder = new IDevAidlInterface.Stub() {
        //other method...
        @Override
        public void addCallbacks(IDevAidlCallbacks callbacks) throws RemoteException {
            if (!mRemoteCallbacksList.contains(callbacks))
                mRemoteCallbacksList.add(callbacks);
            callbacks.asBinder().linkToDeath(new IBinder.DeathRecipient() {
                @Override
                public void binderDied() {
                    mRemoteCallbacksList.remove(callbacks); //客户端死亡时接口清空
                    Log.w(TAG, "binderDied, remove callbacks="+callbacks);
                }
            }, 0);
        }
     }

	//DevManagerProxy.java
	private ServiceConnection mServiceConnection = new ServiceConnection() {
        @SuppressWarnings("unchecked")
        @Override
        public void onServiceConnected(final ComponentName name, final IBinder service) {
            if (mIsRemote) {
                mRemoteBinder = IDevAidlInterface.Stub.asInterface(service);
                try {
                    mRemoteBinder.addCallbacks(mRemoteCallbacks);
                    //other code...
                    mBinded = true;
                } catch (RemoteException e) {
                    e.printStackTrace();
                } catch (NullPointerException e) {
                    e.printStackTrace();
                }

                Log.d(TAG, "connected to the remote service");
            } else {
                mLocalBinder = (DevManagerService.DevLocalBinder) service;
                mLocalBinder.setCallbacks(mLocalCallbacks);
                //other code...
                mBinded = true;
                Log.d(TAG, "connected to the local service");
            }
            mBinding = false;

        }

        @Override
        public void onServiceDisconnected(final ComponentName name) {
            mBinded = false;
            if (mIsRemote) {
                Log.d(TAG, "disconnected from the remote service");
                mRemoteBinder = null;
            } else {
                Log.d(TAG, "disconnected from the local service");
                if (mLocalBinder != null)
                    mLocalBinder.setCallbacks(null);
                mLocalBinder = null;
            }


            mBinding = false;
        }
    };
  1. app端通过DevManagerProxy对象执行如下操作
    • step1 startScan扫描设备
    • step2 选中扫到设备并发起connect
    • step3 设备ready后发送 hello
    • step4 app端收到设备回复 world
	//app客户端
	mDevManagerProxy.startScan();

	
	private DevManagerCallbacks mDevManagerCallbacks = new DevManagerCallbacks() {
        @Override
        public void onDataReceived(BluetoothDevice device, String data) {
            Log.w(TAG, "onDataReceived[" + device.getAddress() + "]:" + data);
        }
               
        @Override
        public void onDevInformationRead(BluetoothDevice device, String manufacturer, String model, String serialNumber, String hardware, String firmware, String software) {
            Log.w(TAG, "onDevInformationRead[" + device.getAddress() + "]:\r\n" +
                    "\t\t\t\tmanufacturer:\t" + manufacturer + "\r\n" +
                    "\t\t\t\tmodel       :\t" + model + "\r\n" +
                    "\t\t\t\tserialNumber:\t" + serialNumber + "\r\n" +
                    "\t\t\t\thardware    :\t" + hardware + "\r\n" +
                    "\t\t\t\tfirmware    :\t" + firmware + "\r\n" +
                    "\t\t\t\tsoftware    :\t" + software + "\r\n");
        }
       
        @Override
        public void onFoundDevice(BluetoothDevice device) {
            /*
             * 扫到一个目标设备 连接设备 停止扫描
             * 如果需要多个设备 可以在app层添加控制数据结构 并保持扫描状态
             */
            Log.w(TAG, "onFoundDevice[" + device.getAddress() + "]:" + device.getName());
            if (MINI_CAR.equalsIgnoreCase(device.getName())) {
                mDevManagerProxy.stopScan();
                mDevManagerProxy.connect(device);
            }
        }
        
        @Override
        public void onDeviceConnected(@NonNull BluetoothDevice device) {
            Log.w(TAG, "onDeviceConnected[" + device.getAddress() + "]");
            //登记已连接的设备,多设备管理用容器存储
            if (mDevice == null) {
                mDevice = device;
            }
        }

        @Override
        public void onDeviceReady(@NonNull BluetoothDevice device) {
            Log.w(TAG, "onDeviceReady[" + device.getAddress() + "]");
            //设备就绪后周期性的发送指令
            mWorkHandler.post(new Runnable() {
                @Override
                public void run() {
                    Log.w(TAG, "send to Dev[" + device.getAddress() + "]:hello");
                    mDevManagerProxy.send(device, "hello".getBytes());
                    mWorkHandler.postDelayed(this, 1000);
                }
            });
        }
        //other callback...
    };
  1. 上述app/service通信时序图:
    在这里插入图片描述

  2. 实测log
    在这里插入图片描述



  • 代理模式的优点

    新框架编程模式如下
    在这里插入图片描述

  1. 无需让activity继承BleProfileServiceReadyActivity,将蓝牙相关的操作封装到代理对象中,实现界面与业务逻辑解耦。
  2. 弃用原框架的服务-设备 一对一模式,改用一对多模式,即 BleProfileService -> BleMulticonnectProfileService
    一对多即包含一对一的特例,并且可以由客户程序自行决定管理设备的个数。
  3. 支持本地和远程服务两种bind模式,灵活运用到各种软件框架下实现高效ble设备控制,尤其适用于无屏幕的交互设备。
  4. 蓝牙状态回调抛弃之前的本地广播方式,采用本地/远程回调方案替代,更加高效且易于移植。


Demo源码地址

本文代码请参考
https://github.com/linx295/DevManagerDemo



总结

本文探讨了:

  • 蓝牙属性服务访问实现的一般方法
  • 官方uart源码的类协作原理
  • 如何使用代理模式改进框架
  • 实现远程与本地bind服务兼容
  • 改进后框架高内聚低耦合的优点
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值