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

更新日期: 2023-08-05 阅读次数: 625 字数: 1437 分类: Android

要给 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 聊聊, 查看更多联系方式