Android BLE 蓝牙监听 STM32WB 上的自定义特性

文章目录

    要给 Android 平板蓝牙语音对讲 APP 上增加一个报警推送功能,即监听蓝牙控制板上的一个信号,来显示报警信息。实际上就是监听一个自定义的特性即可。

    STM32 芯片的 Android SDK 封装的太猛了,捋一遍代码脑子爆炸。 所以记录一下看 SDK 源码的过程。

    特性扫描

    BlueSTSDK/BlueSTSDK/src/main/java/com/st/BlueSTSDK/Node.java

    增加了调试日志,方便查看新增特性的 UUID

    mCharFeatureMap.clear();
    for(BluetoothGattService service : nodeServices){
        //check if it is a specific service
        if(service.getUuid().equals(BLENodeDefines.Services.Debug.DEBUG_SERVICE_UUID))
            mDebugConsole = buildDebugService(service);
        else if(service.getUuid().equals(BLENodeDefines.Services.Config.CONFIG_CONTROL_SERVICE_UUID)) {
            List<BluetoothGattCharacteristic> controlChar = service.getCharacteristics();
            //check for the initialization characteristics
            for(BluetoothGattCharacteristic characteristic : controlChar) {
        	if (characteristic.getUuid().equals(BLENodeDefines.Services.Config.FEATURE_COMMAND_UUID))
        	    mFeatureCommand = characteristic;
        	if (characteristic.getUuid().equals(BLENodeDefines.Services.Config.REGISTERS_ACCESS_UUID))
        	    mConfigControl = new ConfigControl(Node.this, characteristic,mConnection);
            }//for
        }else {//otherwise will contains feature characteristics
            Log.d(TAG, "found service with uuid: " + service.getUuid().toString());
            for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
        	UUID uuid = characteristic.getUuid();
        	Log.d(TAG, "found char with uuid: " + uuid.toString());
        	if (BLENodeDefines.FeatureCharacteristics.isBaseFeatureCharacteristics(uuid)) {
        	    buildFeatures(characteristic);
        	    Log.d(TAG, "found 1");
        	}else if (BLENodeDefines.FeatureCharacteristics.isExtendedFeatureCharacteristics(uuid)) {
        	    buildFeaturesKnownUUID(characteristic,
        		    BLENodeDefines.FeatureCharacteristics.getExtendedFeatureFor(uuid));
        	    Log.d(TAG, "found 2");
        	}else if (BLENodeDefines.FeatureCharacteristics
        		.isGeneralPurposeCharacteristics(uuid)) {
        	    buildGenericFeature(characteristic);
        	    Log.d(TAG, "found 3");
        	}else if(mExternalCharFeatures!=null &&
        		mExternalCharFeatures.containsKey(uuid)) {
        	    buildFeaturesKnownUUID(characteristic, mExternalCharFeatures.get(uuid));
        	    Log.d(TAG, "found 4");
        	}
        	Log.d(TAG, "found 5");
            }//for
    
        }//if-else-if-else
    }//for each service
    

    打印出的日志:

    found service with uuid: 00001801-0000-1000-8000-00805f9b34fb
    found char with uuid: 00002a05-0000-1000-8000-00805f9b34fb
    found 5
    found service with uuid: 00001800-0000-1000-8000-00805f9b34fb
    found char with uuid: 00002a00-0000-1000-8000-00805f9b34fb
    found 5
    found char with uuid: 00002a01-0000-1000-8000-00805f9b34fb
    found 5
    found char with uuid: 00002a04-0000-1000-8000-00805f9b34fb
    found 5
    found service with uuid: 00000000-0001-11e1-9ab4-0002a5d5c51b
    found char with uuid: 00000001-0002-11e1-ac36-0002a5d5c51b
    found 2
    found 5
    found char with uuid: 00000002-0002-11e1-ac36-0002a5d5c51b
    found 2
    found 5
    found service with uuid: d973f2e0-b19e-11e2-9e96-0800200c9a66
    found char with uuid: d973f2e1-b19e-11e2-9e96-0800200c9a66
    found 5
    

    其中 d9 开头的 UUID 即是自定义特性。

    Node.java 中与 Notify 特性相关的函数

    /**
    *
    * test if a characteristics can be notify
    * @param characteristic characteristic to notify
    * @return true if we can receive notification from it
    */
    private static boolean charCanBeNotify(BluetoothGattCharacteristic characteristic)
    
    /**
    * send a request for enable/disable the notification update on a specific characteristics
    * @param characteristic characteristics to notify
    * @param enable true if you want enable the notification, false if you want disable it
    * @return true if the request is correctly send, false otherwise
    */
    boolean changeNotificationStatus(BluetoothGattCharacteristic characteristic, boolean enable)
    
    /**
    * unsubscribe the notification for the node update of the feature
    * @param feature feature that you want stop to be notify
    * @return false if the feature is not handle by this node or disabled
    */
    public boolean disableNotification(Feature feature)
    
    /**
     * ask to the node to notify when the feature change its value
     * @param feature feature to look
     * @return false if the feature is not handle by this node or disabled
     */
    public boolean enableNotification(Feature feature){
        if(!feature.isEnabled() || feature.getParentNode()!=this)
            return false;
        if(isEnableNotification(feature))
            return true;
        BluetoothGattCharacteristic featureChar = getCorrespondingChar(feature);
        if(charCanBeNotify(featureChar)) {
            mNotifyFeature.add(feature);
            //other things are send using that characteristic, so we don't have to
            //enable it
            if(characteristicsHasOtherEnabledFeatures(featureChar,feature))
                return true;
            return changeNotificationStatus(featureChar, true);
        }
        return false;
    }//enableNotification
    

    这个 enable 的写法值得参考一下。

    enableNotification 在我改造后的基于前台服务的 NodeConnectionService 中会被调用。

    注意 enableNotification 的参数是个 Feature 类型。

    适合报警推送的 Feature

    找到 SDK 中内置的一个符合报警推送的 Feature:

    BlueSTSDK/BlueSTSDK/src/main/java/com/st/BlueSTSDK/Features/FeatureSwitch.java

    NodeConnectionService 中与 Notify 相关的函数

    /**
      * listener for the audioSync feature, it will update the synchronize values
      */
    private final Feature.FeatureListener mAudioConfListener = (f, sample) -> {
    	...
    }
    
    protected void enableNeededNotification(@NonNull Node node) {
            NodeServer server = node.getNodeServer();
            if(server == null)
                return;
    
            mAudioTransmitter = server.getExportedFeature(ExportedFeatureAudioOpusVoice.class);
    
            mAudioConf = node.getFeature(FeatureAudioConf.class);
            mAudioConfServer = server.getExportedFeature(ExportedAudioOpusConf.class);
    
            // restoreGuiStatus();  // TODO: startRecSwitch.setChecked(getIsSendingStatus());
    
            if(mAudioConf!=null && mAudioTransmitter!=null ) {
    
                mAudioCodecManager = mAudioConf.instantiateManager(true,false);
                audioSamplingFreq = mAudioCodecManager.getSamplingFreq();
                audioChannels = mAudioCodecManager.getChannels();
    
                mAudioConf.addFeatureListener(mAudioConfListener);
                node.enableNotification(mAudioConf);
    

    addFeatureListener 就是用于监听 notification 的,嘎嘎,终于找到了。

    剩下的就是 enableNotification 的时机问题了。

    实际上在 enableNeededNotification 里最后缀上即可。但是需要提前定义一个自定义的 Feature.FeatureListener 用来监听 notify 过来的报警信息。

    Feature 与 characteristic 关联

    最后一个问题是,如何知道需要 enable 的 Feature 对应的特性值。

    enableNotification 中将 feature 转换为 characteristic 使用了 getCorrespondingChar 这个函数。

    BluetoothGattCharacteristic featureChar = getCorrespondingChar(feature);
    

    在 Node.java 中找到了

    /**
     * find the the gattCharacteristics corresponding to a feature
     * @param feature feature to search
     * @return null if the feature is not handle by the node, the characteristics otherwise
     */
    private BluetoothGattCharacteristic getCorrespondingChar(Feature feature){
        ArrayList<BluetoothGattCharacteristic> candidateChar = new ArrayList<>();
        for (Map.Entry<BluetoothGattCharacteristic,List<Feature>> e: mCharFeatureMap.entrySet()){
            List<Feature> featureList = e.getValue();
            if(featureList.contains(feature)){
                candidateChar.add(e.getKey());
            }
        }//for entry
        if(candidateChar.isEmpty())
            return null;
        else if(candidateChar.size()==1){
            return candidateChar.get(0);
        }else{ //we have to select the feature that permit us to have more data
            int maxNFeature=0;
            BluetoothGattCharacteristic bestChar=null;
            for(BluetoothGattCharacteristic characteristic: candidateChar){
                int nFeature = mCharFeatureMap.get(characteristic).size();
                if(nFeature>maxNFeature){
                    maxNFeature=nFeature;
                    bestChar=characteristic;
                }//if
            }//for
            return bestChar;
        }//if-else
    }
    

    mCharFeatureMap 的类型定义

    private Map<BluetoothGattCharacteristic,List<Feature>> mCharFeatureMap= new HashMap<>();
    

    即,characteristic 对应的是个 Feature list.

    哪些地方填充了 mCharFeatureMap

    • buildFeaturesKnownUUID。最终还是在服务发现回调那里调用了这个函数
    • buildFeatures(BluetoothGattCharacteristic characteristic)
    • buildGenericFeature(BluetoothGattCharacteristic characteristic)
      /** * describe as manage some specific UUID using a feature class, the uuid will be manage by * the node class only if is know before the connection * if a uuid is already know it will be overwrite with the new list of feature * @param userDefineFeature map that link the uuid with the features that contains. */ public void addExternalCharacteristics(@Nullable Map<UUID,List<Class< ? extends Feature>>> userDefineFeature){ if(userDefineFeature==null) return; mExternalCharFeatures.putAll(userDefineFeature); }//addExternalCharacteristics

    看来是需要调用这个 addExternalCharacteristics

    }else if(mExternalCharFeatures!=null &&
    	mExternalCharFeatures.containsKey(uuid)) {
        buildFeaturesKnownUUID(characteristic, mExternalCharFeatures.get(uuid));
        Log.d(TAG, "found 4");
    }
    

    何时调用 addExternalCharacteristics

    Node 建立连接时有个 options

    public void connect(Context c, ConnectionOption options){
        //if we are already connected or we are connecting avoid do to send again the command
        if(mState == State.Connected || mState==State.Connecting){
            return;
        }
        updateNodeStatus(State.Connecting);
        if(options == null)
            options = ConnectionOption.buildDefault();
        //we start the connection so we will stop to receive advertise, so we delete the timeout
        if(mBackGroundHandler !=null) mBackGroundHandler.removeCallbacks(mSetNodeLost);
        mUserAskToDisconnect=false;
        /*
        HandlerThread thread = new HandlerThread("NodeConnection");
        thread.start();
        mBleThread = new Handler(thread.getLooper());
        */
        mBleThread = new Handler(Looper.getMainLooper());
        mContext=c;
        setBoundListener(c.getApplicationContext());
        mConnectionOption = options;
        addExternalCharacteristics(options.getUserDefineFeature());
        mBleThread.post(mConnectionTask);
    }
    

    在 NodeConnectionService 中搜索 node.connect

    private void connect(int startId, Intent intent) {
        String tag = intent.getStringExtra(NODE_TAG_ARG);
        Log.d("NodeConnectionService","connect " + tag);
        ConnectionOption options = intent.getParcelableExtra(CONNECTION_PARAM_ARG);
        Node n = Manager.getSharedInstance().getNodeWithTag(tag);
        if(n!=null)
            if(!mConnectedNodes.contains(n)) {
                mConnectedNodes.add(n);
                mNode = n;
                n.addNodeStateListener(mStateListener);
                n.connect(this,options);
                startForeground(startId, buildConnectionNotification(n));
            }
    }
    

    看到 ConnectionOption.java 时,我绝望了。。。

    private final
    UUIDToFeatureMap userDefineFeature;
    
    private ConnectionOption(Parcel in) {
        resetCache = in.readByte() != 0;
        enableAutoConnect = in.readByte() != 0;
        if(in.readByte()!=0){
            userDefineFeature = (UUIDToFeatureMap) in.readSerializable();
        }else{
            userDefineFeature = null;
        }
    }
    

    这是什么鬼玩意,我要直接改 SDK 源代码了,封装这么多层,真是浪费时间。

    最终

    直接修改 sdk 中的服务发现逻辑,增加了一个 uuid 的判断逻辑

    }else if(mExternalCharFeatures!=null &&
        mExternalCharFeatures.containsKey(uuid)) {
        buildFeaturesKnownUUID(characteristic, mExternalCharFeatures.get(uuid));
        Log.d(TAG, "found 4");
    } else if (uuid.toString().equals("d973f2e1-b19e-11e2-9e96-0800200c9a66")) {
        // 报警通知特性
        //List<Feature> tempFeatures = new ArrayList<>(FeatureSwitch.class);
        List<Class<? extends Feature>> featureClasses = new ArrayList<>();
        featureClasses.add(FeatureSwitch.class);
        buildFeaturesKnownUUID(characteristic, featureClasses);
    }
    

    java 字符串比对的坑

    开始时使用双等于号判定,蓝牙特性 uuid 是否等于固定的字符串,发现怎么也无法匹配。

    原来是必须使用 equals 来判定是否相等。。。

    关于作者 🌱

    我是来自山东烟台的一名开发者,有感兴趣的话题,或者软件开发需求,欢迎加微信 zhongwei 聊聊,或者关注我的个人公众号“大象工具”, 查看更多联系方式