AAOS CarMediaService 问题分析
文章目录
- 问题描述
- 车载蓝牙音乐流程
- Music 监听焦点变化流程
- BT请求焦点的流程
- MediaSession 服务端的流程
- BT和music 之间的相互影响
问题描述
-
问题
AAOS界面连接蓝牙的情况下,Music应用播放音乐会暂停。
-
分析
暂停是应用的行为,Music应用会监听focus的变化,监听到焦点失去的情况会调用暂停。但是Music 应用刚启动播放的时候 也会请求焦点,焦点第一次是在bt这边的。bt失去焦点,但立马又重新请求了焦点。 BT请求焦点就导致Music应用失去焦点而暂停。
了解问题之前首先要理解
- 蓝牙音乐在哪里调用到音频框架?
- 音频焦点
- carMediaService
车载蓝牙音乐流程
-
A2DP端
代码位置:
system\bt\btif\src\btif_avrcp_audio_track.cc
system\bt\stack\a2dp\a2dp_aac.cc
在上述的BtifAvrcpAudioTrackCreate()函数中。在这里面会创建一个AAudioStreamBuilder, AAudio 通过legacy的模式 的audiotrack 来进行处理 写数据。 -
AAudio端
蓝牙音乐, 车载端是一个sink 端,作为播放来使用。 对应的流程在btif_avrcp_audio_track 是通过调用AAudio的接口来实现播放。其中AAudio 没有实现mmap的方式 走的是legacy模式
代码在frameworks\av\media\libaaudio\src\legacy\AudioStreamTrack.cpp中
也就是通过创建audiotrack,然后设置参数、往里面写数据实现的。audiotrack start 的时候 同样会getoutputAttr获取到设备, 这个时候的路由信息已经由AAOS 根据car_audio_policy.xml
进行注册。 -
整体流程:
从source端(也就是手机通过蓝牙)发送过来的是aac或者ldac编码的数据, 数据在a2dp中继续解码 并不是进入到audiotrack。 a2dp中有相当于播放器中解码器的功能,解码后的数据才调用audiotrak进行播放。
Music 监听焦点变化流程
- 实现AudioFocusListener 然后注册到AudioMananger
private OnAudioFocusChangeListener mAudioFocusListener = new OnAudioFocusChangeListener() {public void onAudioFocusChange(int focusChange) {mMediaplayerHandler.obtainMessage(FOCUSCHANGE, focusChange, 0).sendToTarget();}};mAudioManager.requestAudioFocus(mAudioFocusListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
-
在框架层焦点发生变化的时候 回调到外部注册进去的listener
-
Music 中MediaPlayBackService的实现
通过looper发送消息进行处理,而如果是focus丢失的时候,所做的操作是pause。
case AudioManager.AUDIOFOCUS_LOSS:
Log.v(LOGTAG, "AudioFocus: received AUDIOFOCUS_LOSS");if (isPlaying()) {mPausedByTransientLossOfFocus = false;}pause();
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:Log.v(LOGTAG, "AudioFocus: received AUDIOFOCUS_LOSS_TRANSIENT");if (isPlaying()) {mPausedByTransientLossOfFocus = true;}pause();
break;
BT请求焦点的流程
- 监听mediassion 的onPrepare事件
有Prepare事件发生后会调用requestFocus。其usage是USAGE_MEDIA。这个会回调到AAOS的CarAudioFocus
进行处理, 其就是根据交互矩阵进行处理的。当前持有的是music应用,在BT 请求焦点后,会发送消息通知music 焦点失去了。
在上面的流程知道 失去焦点后 会调用player的 pause 进行暂停操作。
packages\apps\Bluetooth\src\com\android\bluetooth\avrcpcontroller\AvrcpControllerStateMachine.java
BluetoothMediaBrowserService.addressedPlayerChanged(mSessionCallbacks);
sBluetoothMediaBrowserService.mSession.setCallback(callback);MediaSessionCompat.Callback mSessionCallbacks = new MediaSessionCompat.Callback() {@Overridepublic void onPrepare() {logD("onPrepare");A2dpSinkService a2dpSinkService = A2dpSinkService.getA2dpSinkService();if (a2dpSinkService != null) {a2dpSinkService.requestAudioFocus(mDevice, true);}}
}private synchronized int requestAudioFocus() {if (DBG) Log.d(TAG, "requestAudioFocus()");// Bluetooth A2DP may carry Music, Audio Books, Navigation, or other sounds so mark content// type unknown.AudioAttributes streamAttributes =new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN).build();// Bluetooth ducking is handled at the native layer at the request of AudioManager.AudioFocusRequest focusRequest =new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).setAudioAttributes(streamAttributes).setOnAudioFocusChangeListener(mAudioFocusListener, this).build();int focusRequestStatus = mAudioManager.requestAudioFocus(focusRequest);// If the request is granted begin streaming immediately and schedule an upgrade.if (focusRequestStatus == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {startFluorideStreaming();mAudioFocus = AudioManager.AUDIOFOCUS_GAIN;}return focusRequestStatus;}
- 那现在的问题是 哪里触发了onPrepare ?
可以看到是实现了MediaSession的Callback。 理解MediaSession的概念
MediaSession 有客户端和服务端。 客户端对应的是UI 这一端,服务端对应的是player。
UI这一端的实现是在packages/app/Car/Media 中。
主要是几个类 的封装分别是
MediaControl:用来控制MediaSession, 在上面的AVrcpControl中 实现的MessaionCompat的callback回调的
play prepare都是由MediaControl这边调用的。
packages\apps\Car\Media\src\com\android\car\media\service\MediaConnectorService.java
packages\apps\Car\libs\car-media-common\src\com\android\car\media\common\playback\PlaybackViewModel.java
MediaControllerCompat.TransportControls controls = controller.getTransportControls();
controls.prepare();
这里的prepare 会调用到AvrcpControllerStateMachine的onPrepare中。
- 如何监听到playstate的变化的?
packages\apps\Music\src\com\android\music\MediaPlaybackService.java
可以确认是music 发送的play state 导致 carMusicApp这边MediaControl调用prepare的.
下面的RemoteControlClient实际是MediaSession的封装
private void notifyChange(String what) {Intent i = new Intent(what);i.putExtra("id", Long.valueOf(getAudioId()));i.putExtra("artist", getArtistName());i.putExtra("album", getAlbumName());i.putExtra("track", getTrackName());i.putExtra("playing", isPlaying());sendStickyBroadcast(i);if (what.equals(PLAYSTATE_CHANGED)) {mRemoteControlClient.setPlaybackState(isPlaying()? RemoteControlClient.PLAYSTATE_PLAYING: RemoteControlClient.PLAYSTATE_PAUSED);}
代码位置:
packages\services\Car\service\src\com\android\car\CarMediaService.java
是通过重新编写MediaController.Callback来实现的。MediaSession设置的状态变化会通过callback调用到MediaCotroller当中
private class MediaControllerCallback extends MediaController.Callback {public void onPlaybackStateChanged(@Nullable PlaybackState state) {setPrimaryMediaSource(mediaSource, MEDIA_SOURCE_MODE_PLAYBACK);}
}private void startMediaConnectorService(boolean startPlayback, UserHandle currentUser) {Intent serviceStart = new Intent(MEDIA_CONNECTION_ACTION);Log.d(CarLog.TAG_MEDIA, Log.getStackTraceString(new Throwable()));serviceStart.setPackage(mContext.getResources().getString(R.string.serviceMediaConnection));serviceStart.putExtra(EXTRA_AUTOPLAY, startPlayback);mContext.startForegroundServiceAsUser(serviceStart, currentUser);}
packages\apps\Car\Media\src\com\android\car\media\service\MediaConnectorService.java
在上述的startMediaConnectorService会启动一个service 这个service调用到MediaConnectorSerive中onStartCommand。
在startcommad 中会使用mediacontrol 进行prepare操作。
public int onStartCommand(Intent intent, int flags, int startId) {playbackViewModel.getPlaybackStateWrapper().observe(this,playbackStateWrapper -> {if (playbackStateWrapper != null) {// If the source to play was specified in the intent ignore others.ComponentName intentComp = mCurrentTask.mMediaComp;ComponentName stateComp = playbackStateWrapper.getMediaSource().getBrowseServiceComponentName();if (!Objects.equals(stateComp, intentComp)) {return;}if (playbackStateWrapper.isPlaying()) {stopTask();return;}if ((playbackStateWrapper.getSupportedActions()& PlaybackStateCompat.ACTION_PREPARE) != 0) {playbackViewModel.getPlaybackController().getValue().prepare();if (!autoplay) {stopTask();}}if (autoplay && (playbackStateWrapper.getSupportedActions()& PlaybackStateCompat.ACTION_PLAY) != 0) {playbackViewModel.getPlaybackController().getValue().play();stopTask();}}});
src\com\android\bluetooth\a2dpsink\A2dpSinkService.java
总结: 单单看MediaSession 和 MediaControl。 MediaControl是UI端控制Service端的类,在AAOS中所有的app播放控制客户端的实现都是carMediaApp中MediaControl的实现的(包括蓝牙audio localplayer界面中暂停播放,下一首 上一首等等)。 MediaSession是服务端, 这个服务端包括(蓝牙的src\com\android\bluetooth
,和/apps/Car/LocalMediaPlayer)。这这里面实现了Mediassion 的callback 用来响应client 端UI的控制。 而响应之后的状态改变可以通过继承MediaControl的callback 在客户端实现。
而Music应用中会向session发送状态改变的消息,客户端carMediaApp会响应这个消息,响应这个消息的结果就是prepare播放器。这个prepare调用到蓝牙的MediaSeesion。MediaSeeion正常应该 一个客户端和服务器一一对应的。
-
问题的解决
对于没有MediaSource的session变化 不启动MediaConnectService。
MediaSession 服务端的流程
首先实现MediaSessionCompat.Callback(),然后将这个callback 设置到MediaBrowserService的sesseion中。
BluetoothMediaBrowserService.addressedPlayerChanged(mSessionCallbacks);
sBluetoothMediaBrowserService.mSession.setCallback(callback);
session 中token的传递
- 为什么music 发送的消息 这边的session 可以接收到。
因为在CarMediaService 中已经注册了MediaSession变化的消息。 在音乐应用启动的时候 会新建MediaSession,
而在这里就会监听了active MediaSession的变化,同时传递当前所有的Mediacontrol, 然后对这些mediaControl注册callback。
在这个callback 中监听onPlaybackStateChanged事件。 而在这边对mediacontrol的管理是通过token实现的。
token 是在MediaSession 和 MdiaControl 直接建立连接的数据。可以通过getHashCode来打印其hash值确认。
private void initUser(@UserIdInt int userId) {updateMediaSessionCallbackForCurrentUser();if (mSessionsListener != null) {mMediaSessionManager.removeOnActiveSessionsChangedListener(mSessionsListener);}mSessionsListener = new SessionChangedListener(ActivityManager.getCurrentUser());UserHandle currentUserHandle = new UserHandle(ActivityManager.getCurrentUser());mMediaSessionManager.addOnActiveSessionsChangedListener(null, currentUserHandle,new HandlerExecutor(mHandler), mSessionsListener);}private class SessionChangedListener implements OnActiveSessionsChangedListener {private final int mCurrentUser;SessionChangedListener(int currentUser) {mCurrentUser = currentUser;}@Overridepublic void onActiveSessionsChanged(List<MediaController> controllers) {if (ActivityManager.getCurrentUser() != mCurrentUser) {Slog.e(CarLog.TAG_MEDIA, "Active session callback for old user: " + mCurrentUser);return;}Log.d(CarLog.TAG_MEDIA, Log.getStackTraceString(new Throwable()));Log.d(CarLog.TAG_MEDIA, "controllers szie " + controllers.size());mMediaSessionUpdater.registerCallbacks(controllers);}}private void registerCallbacks(List<MediaController> newControllers) {List<MediaController> additions = new ArrayList<>(newControllers.size());Map<MediaSession.Token, MediaControllerCallback> updatedCallbacks =new HashMap<>(newControllers.size());for (MediaController controller : newControllers) {MediaSession.Token token = controller.getSessionToken();String newPackageName = controller.getPackageName();Log.d(CarLog.TAG_MEDIA, Log.getStackTraceString(new Throwable()));MediaControllerCallback callback = mCallbacks.get(token);if (callback == null) {callback = new MediaControllerCallback(controller);callback.register();additions.add(controller);}updatedCallbacks.put(token, callback);}private MediaControllerCallback(MediaController mediaController) {public void onPlaybackStateChanged(@Nullable PlaybackState state) {}
BT和music 之间的相互影响
- 本地音乐在播放,手机播放蓝牙音乐 两个声音同时播放
这个是焦点管理的问题, 按理理解蓝牙音乐播放的时候 应该去请求焦点。需要设置属性才会请求,默认不会请求。
播放的时候 前面流程不用管,最后会调用到SRC_STR_START中,没有请求焦点,music应用就会一直播。修改方法:
解决方法:通过配置shouldRequestFocus 让BT应用在每次播放的时候都强制请求焦点解决。强制请求之后,music就会失去焦点暂停。
packages\apps\Bluetooth\src\com\android\bluetooth\a2dpsink\A2dpSinkStreamHandler.javaswitch (message.what) {case SRC_STR_START:mStreamAvailable = true;if (isTvDevice() || shouldRequestFocus()) {requestAudioFocusIfNone();}break;
- 蓝牙音乐在播放的时候,本地音乐播放会导致蓝牙直接stop掉。
这个stop 是在蓝牙的MediaSessions callback onstop中调用的。
而这个回调是在CarMediaService中被触发的。也是在上面的流程中music应用的playbackstate 回调中stop的。
解决方法:不调用stop, 调用pause进行暂停。
public void onPlaybackStateChanged(@Nullable PlaybackState state)private void setPlaybackMediaSource(ComponentName playbackMediaSource) {stopAndUnregisterCallback();
相关文章:
AAOS CarMediaService 问题分析
文章目录 问题描述车载蓝牙音乐流程Music 监听焦点变化流程BT请求焦点的流程MediaSession 服务端的流程BT和music 之间的相互影响 问题描述 问题 AAOS界面连接蓝牙的情况下,Music应用播放音乐会暂停。 分析 暂停是应用的行为,Music应用会监听focus的变化…...
06-Flask-蓝图的使用
蓝图的使用 前言蓝图使用方式 前言 本篇来学习下Flask中蓝图的使用 蓝图 在Flask中使用蓝图(Blurprint)来分模块组织管理蓝图可以理解为存储一组视图方法的容器对象,特点如下: 一个应用可以具有多个Blueprint可以将一个Blueprint注册到任何一个未使用…...

【LeetCode力扣】189 53 轮转数组 | 最大子数组和
目录 1、189. 轮转数组 1.1、题目介绍 1.2、解题思路 2、53. 最大子数组和 2.1、题目介绍 2.2、解题思路 1、189. 轮转数组 1.1、题目介绍 原题链接:189. 轮转数组 - 力扣(LeetCode) 示例 1: 输入: nums [1,2,3,4,5,6,7], k 3输…...

Go学习第十七章——Gin中间件与路由
Go web框架——Gin中间件与路由 1 单独注册中间件1.1 入门案例1.2 多个中间件1.3 中间件拦截响应1.4 中间件放行 2 全局注册中间件3 自定义参数传递4 路由分组4.1 入门案例4.2 路由分组注册中间件4.3 综合使用 5 使用内置的中间件6 中间件案例权限验证耗时统计 1 单独注册中间件…...

真实感渲染的非正式调研与近期热门研究分享
真实感渲染的非正式调研与近期热门研究分享 1 期刊1 Top2 Venues 2 Rendering Reserach1 Material2 BRDF3 Appearance Modeling4 Capture5 Light Transport光线传播6 Differetiable Rendring-可微渲染7 Ray Tracing8 Denoising降噪9 NeRF 3 VR/AR4 Non-Photorealistic Renderin…...
matlab中字符串转换为数字(str2double函数)
str2double函数 将 str 中的文本转换为双精度值。str 包含表示实数或复数值的文本。str 可以是字符向量、字符向量元胞数组或字符串数组。如果 str 是字符向量或字符串标量,则 X 是数值标量。如果 str 是字符向量元胞数组或字符串数组,则 X 是与 str 具…...

基于java的ssm框架农夫果园管理系统设计与实现
项目描述 临近学期结束,还是毕业设计,你还在做java程序网络编程,期末作业,老师的作业要求觉得大了吗?不知道毕业设计该怎么办?网页功能的数量是否太多?没有合适的类型或系统?等等。这里根据疫情当下,你想解决的问…...

ctf md5爆破
1.知道组成的字符为数字,然后知道加密后的MD5,求组成的字符 import hashlibimport stringdef crackMd5(dst):dst dst.lower()for a in range(0,10):for b in range(0,10):for c in range(0,10):for d in range(0,10):word str(a) str(b) str(c) str(d) "_heetian&q…...

不同碳化硅晶体面带来的可能性
对于非立方晶体,它们天生具有各向异性,即不同方向具有不同的性质。以碳化硅晶体面为例: 4H-SIC和6H-SIC的空间群是P63mc,点群是6mm。两者都属于六方晶系,具有各向异性。3C-SIC的空间群是F-43m,点群是-43m。…...

Kafka集群
Kafka集群 1、Kafka 概述1.1消息队列背景1.2类型1.3Kafka 定义1.4Kafka 简介 2、消息队列好处3、消息队列的模式4、Kafka 的特性5、Kafka 系统架构4、部署 kafka 集群4.1下载安装包4.2 安装 Kafka4.2.1 修改配置文件4.2.2 修改环境变量4.2.3 配置 zookeeper启动脚本4.2.4 设置…...

国腾GM8775C完全替代CS5518 MIPIDSI转2 PORT LVDS
集睿致远CS5518描述: CS5518是一款MIPI DSI输入、LVDS输出转换芯片。MIPI DSI 支持多达4个局域网,每条通道以最 大 1Gbps 的速度运行。LVDS支持18位或24位像素,25Mhz至154Mhz,采用VESA或JEIDA格 式。它只能使用单个1.8v电源&am…...
搜索与图论:匈牙利算法
将所有点分成两个集合,使得所有边只出现在集合之间,就是二分图 二分图:一定不含有奇数个点数的环;可能包含长度为偶数的环, 不一定是连通图 二分图的最大匹配: #include<iostream> #include<cs…...
明星艺人类的百度百科怎么创建 ?
明星艺人们的知名度对于其事业的成功至关重要,而作为国内最大的中文百科全书网站,百度百科成为了人们获取信息的重要来源。一线明星当然百科不用自己操心,平台和网友就给维护了,但是刚刚走红的明星艺人应提早布局百科词条…...

类EMD的“信号分解方法”及MATLAB实现(第八篇)——离散小波变换DWT(小波分解)
在之前的系列文章里,我们介绍了EEMD、CEEMD、CEEMDAN、VMD、ICEEMDAN、LMD、EWT,我们继续补完该系列。 今天要讲到的是小波分解,通常也就是指离散小波变换(Discrete Wavelet Transform, DWT)。在网上有一些介绍该方法…...

python随手小练10(南农作业题)
题目1: 编写程序,输出1~1000之间所有能被4整除,但是不能被5整除的数 具体操作: for i in range(1,1000): #循环遍历1~999,因为range是左闭右开if (i % 4 0) and (i % 5 ! 0) :print(i) 结果展示: 题目2&…...
How to install mongodb-7.0 as systemd service with podman
How to install mongodb-7.0 as systemd service with podman 1、安装1.1、创建卷1.2、配置文件1.3、创建容器1.4、服务管理1.5、容器管理 2、客户端管理 1、安装 1.1、创建卷 配置卷 podman volume create --label typemongo-7.0 --label envdev mongo-7.0-conf数据卷 pod…...
一文彻底理解python浅拷贝和深拷贝
目录 一、必备知识二、基本概念三、列表,元组,集合,字符串,字典浅拷贝3.1 列表3.2 元组3.3 集合3.4 字符串3.5 字典3.6 特别注意浅拷贝总结 四、列表,元组,集合,字符串,字典深拷贝 一…...
什么是软件的生命周期?全方位解释软件的生命周期
软件的生命周期 软件生命周期是指从软件产品的设想开始到软件不再使用而结束的时间。 如果把软件看成是有生命的事 物,那么软件的生命周期可以分成6个阶段,即需求分析、计划、设计、编码、测试、运行维护 需求分析阶段: 分析需求的可行性&…...

网络安全—小白自学
1.网络安全是什么 网络安全可以基于攻击和防御视角来分类,我们经常听到的 “红队”、“渗透测试” 等就是研究攻击技术,而“蓝队”、“安全运营”、“安全运维”则研究防御技术。 2.网络安全市场 一、是市场需求量高; 二、则是发展相对成熟…...

List 3.5 详解原码、反码、补码
前言 欢迎来到我的博客,我是雨空集(全网同名),无论你是无意中发现我,还是有意搜索而来,我都感到荣幸。这里是一个分享知识、交流想法的平台,我希望我的博客能给你带来帮助和启发。如果你喜欢我…...

循环冗余码校验CRC码 算法步骤+详细实例计算
通信过程:(白话解释) 我们将原始待发送的消息称为 M M M,依据发送接收消息双方约定的生成多项式 G ( x ) G(x) G(x)(意思就是 G ( x ) G(x) G(x) 是已知的)࿰…...

Cilium动手实验室: 精通之旅---20.Isovalent Enterprise for Cilium: Zero Trust Visibility
Cilium动手实验室: 精通之旅---20.Isovalent Enterprise for Cilium: Zero Trust Visibility 1. 实验室环境1.1 实验室环境1.2 小测试 2. The Endor System2.1 部署应用2.2 检查现有策略 3. Cilium 策略实体3.1 创建 allow-all 网络策略3.2 在 Hubble CLI 中验证网络策略源3.3 …...

深入理解JavaScript设计模式之单例模式
目录 什么是单例模式为什么需要单例模式常见应用场景包括 单例模式实现透明单例模式实现不透明单例模式用代理实现单例模式javaScript中的单例模式使用命名空间使用闭包封装私有变量 惰性单例通用的惰性单例 结语 什么是单例模式 单例模式(Singleton Pattern&#…...
【ROS】Nav2源码之nav2_behavior_tree-行为树节点列表
1、行为树节点分类 在 Nav2(Navigation2)的行为树框架中,行为树节点插件按照功能分为 Action(动作节点)、Condition(条件节点)、Control(控制节点) 和 Decorator(装饰节点) 四类。 1.1 动作节点 Action 执行具体的机器人操作或任务,直接与硬件、传感器或外部系统…...
镜像里切换为普通用户
如果你登录远程虚拟机默认就是 root 用户,但你不希望用 root 权限运行 ns-3(这是对的,ns3 工具会拒绝 root),你可以按以下方法创建一个 非 root 用户账号 并切换到它运行 ns-3。 一次性解决方案:创建非 roo…...
AspectJ 在 Android 中的完整使用指南
一、环境配置(Gradle 7.0 适配) 1. 项目级 build.gradle // 注意:沪江插件已停更,推荐官方兼容方案 buildscript {dependencies {classpath org.aspectj:aspectjtools:1.9.9.1 // AspectJ 工具} } 2. 模块级 build.gradle plu…...
【Java学习笔记】BigInteger 和 BigDecimal 类
BigInteger 和 BigDecimal 类 二者共有的常见方法 方法功能add加subtract减multiply乘divide除 注意点:传参类型必须是类对象 一、BigInteger 1. 作用:适合保存比较大的整型数 2. 使用说明 创建BigInteger对象 传入字符串 3. 代码示例 import j…...

20个超级好用的 CSS 动画库
分享 20 个最佳 CSS 动画库。 它们中的大多数将生成纯 CSS 代码,而不需要任何外部库。 1.Animate.css 一个开箱即用型的跨浏览器动画库,可供你在项目中使用。 2.Magic Animations CSS3 一组简单的动画,可以包含在你的网页或应用项目中。 3.An…...
现有的 Redis 分布式锁库(如 Redisson)提供了哪些便利?
现有的 Redis 分布式锁库(如 Redisson)相比于开发者自己基于 Redis 命令(如 SETNX, EXPIRE, DEL)手动实现分布式锁,提供了巨大的便利性和健壮性。主要体现在以下几个方面: 原子性保证 (Atomicity)ÿ…...

【p2p、分布式,区块链笔记 MESH】Bluetooth蓝牙通信 BLE Mesh协议的拓扑结构 定向转发机制
目录 节点的功能承载层(GATT/Adv)局限性: 拓扑关系定向转发机制定向转发意义 CG 节点的功能 节点的功能由节点支持的特性和功能决定。所有节点都能够发送和接收网格消息。节点还可以选择支持一个或多个附加功能,如 Configuration …...