【转】ZigBee终端入网方式深入分析
前述
继之前对终端Direct Join的分析,发现很多东西还很模糊,存在很多问题。终于找到时间继续深入挖下去,这次应该比较完整地搞清了终端的入网机制,并纠正之前的几个认识偏差。
由于Z-Stack网络层并不开源,所以一些地方是靠的推测,很多地方的结论也没有实验验证,谨留给诸君参考和斧正。
ZigBee2007协议规范分析
先来看看ZigBee2007协议规范是怎样规定入网请求的:
The semantics of this primitive are as follows:
NLME-JOIN.request
{
ExtendedPANId,
RejoinNetwork,
ScanChannels,
ScanDuration,
CapabilityInformation,
SecurityEnable
}The next higher layer of a device generates this primitive to request to:
- Join a network using the MAC association procedure.
- Join or rejoin a network using the orphaning procedure.
- Join or rejoin a network using the NWK rejoin procedure.
- Switch the operating channel for a device that is joined to a network.
就此原语的描述可以看出,前三种情况均为设备入网的方式,最后一个是为设备切换信道所用,暂不考虑。所以ZigBee设备入网有三种方式,我们分别称之为Join、Orphan Join、Rejoin。三种方式RejoinNetwork参数分别设置为0x00、0x01、0x02。
Join入网过程。首先发起Network Discovery,返回所有应答的节点的信息。在发现的结果中找出符合要求(这里的要求是一些最基本的条件,详见ZigBee协议规范3.6.1.4.1.1节,下同)的父节点,向它发送入网请求。父节点分配16位网络地址。
Rejoin入网过程。发起Network Discovery,在应答的节点中挑选出和自己的ExtendedPANID相同的节点,在这些节点中找出符合要求的父节点,发送入网请求,并且使用自己已拥有的16位网络地址(若没有,则随机生成一个)。
Orphan Join过程。发起Orphan Scan,寻找邻居表中保存有本设备IEEE地址的父节点,在返回结果中找出符合要求的父节点,发送入网请求。父节点返回邻居表中保存的16位网络地址。
可以看出三种入网的过程都可以归纳为网络扫描+选择目标。三者的选择的筛选条件是递增的:任何节点—>指定PANID的节点—>邻居表中有自己信息的节点。
Z-Stack协议栈分析
版本号:ZStack-CC2530-2.5.1a
1. 第一步 扫描
下面是设备启动的函数ZDO_StartDevice,它是设备入网流程的入口,这个函数仅在ZDApp_event_loop事件轮询函数中发生ZDO_NETWORK_INIT事件的时候被调用,而ZDApp_NetworkInit函数就是用来延时发送ZDO_NETWORK_INIT事件的,所以ZDApp_NetworkInit函数也是设备入网过程的触发,这个函数下面将被用到。
这里我只把与终端启动的相关代码贴了出来:
/*********************************************************************
* @fn ZDO_StartDevice
*
* @brief This function starts a device in a network.
*
* @param logicalType - Device type to start
* startMode - indicates mode of device startup
*
* @return none
*/
void ZDO_StartDevice( byte logicalType, devStartModes_t startMode, byte beaconOrder, byte superframeOrder )
{
ZStatus_t ret;
ret = ZUnsupportedMode;
if ( (startMode == MODE_JOIN) || (startMode == MODE_REJOIN) )
{
devState = DEV_NWK_DISC;
ret = NLME_NetworkDiscoveryRequest( zgDefaultChannelList, zgDefaultStartingScanDuration );
}
else if ( startMode == MODE_RESUME ) //Orphan Join
{
devState = DEV_NWK_ORPHAN;
ret = NLME_OrphanJoinRequest( zgDefaultChannelList,
zgDefaultStartingScanDuration );
}
if ( ret != ZSuccess )
{
osal_start_timerEx(ZDAppTaskID, ZDO_NETWORK_INIT, NWK_RETRY_DELAY );
}
}
从上面可以看出,终端的入网第一步就是调用了这两个函数NLME_NetworkDiscoveryRequest、
NLME_OrphanJoinRequest(放到第3步再看),而Join和Rejoin方式的这一部分是完全相同的。从TI的API手册中可以查到:
NLME_NetworkDiscoveryRequest()
此函数请求网络层寻找相邻路由器。这个函数应该在加入并执行网络扫描前调用。扫描确认结果将被返回到ZDO_NetworkDiscoveryConfirmCB()回调函数中。……
2. 扫描结果
在ZDO_NetworkDiscoveryConfirmCB()回调函数中发现,就做了一件事,就是向ZDApp_event_loop发送ZDO_NWK_DISC_CNF事件,直接找到ZDO_NWK_DISC_CNF事件的处理函数(为了方便分析,只留下了关键的函数名):
case ZDO_NWK_DISC_CNF:
if (devState != DEV_NWK_DISC)
break;
if ( ZG_BUILD_JOINING_TYPE && ZG_DEVICE_JOINING_TYPE )
{
networkDesc_t *pChosenNwk;
if ( ( (pChosenNwk = ZDApp_NwkDescListProcessing()) != NULL ) &&
(zdoDiscCounter > NUM_DISC_ATTEMPTS) )
{
if ( devStartMode == MODE_JOIN )
{
devState = DEV_NWK_JOINING;
if ( NLME_JoinRequest( pChosenNwk->…… ) != ZSuccess )
{
ZDApp_NetworkInit(…… );
}
}
else if ( devStartMode == MODE_REJOIN )
{
devState = DEV_NWK_REJOIN;
if ( _NIB.nwkDevAddress == INVALID_NODE_ADDR )
{
// Before trying to do rejoin,
// check if the device has a valid short address
// If not, generate a random short address for itself
}
if ( _NIB.nwkPanId == INVALID_PAN_ID )
{
// Check if the device has a valid PanID,
// if not, set it to the discovered Pan
}
if ( NLME_ReJoinRequest( ……) != ZSuccess )
{
ZDApp_NetworkInit( …… );
}
}
}
else
{
if ( continueJoining )
{
zdoDiscCounter++;
ZDApp_NetworkInit( …… );
}
}
}
break;
通过简化了的代码可以看出,对于扫描结果的处理是这样一个流程:首先需进行至少NUM_DISC_ATTEMPTS次扫描,每次都调用ZDApp_NetworkInit进行重新扫描,如果找到了合格的父节点(pChosenNwk = ZDApp_NwkDescListProcessing()) != NULL),就依照MODE_JOIN或 MODE_REJOIN 分别调用NLME_JoinRequest或NLME_ReJoinRequest向目标父节点发送请求。由于后者的请求中要附带自己的PANID和ShortAddress,所以要事先检查和处理。
从这里可以看出,不管是Join还是Rejoin,如果找不到可用的父节点,将持续调用ZDApp_NetworkInit扫描网络,陷入死循环。
3. 加入父节点
着眼到NLME_JoinRequest和NLME_ReJoinRequest,以及前面的NLME_OrphanJoinRequest上,从TI的API手册中可以查到:
NLME_OrphanJoinRequest()
此函数请求网络层孤立地连接到网络上。此函数是一个默示加入形式的扫描。此函数的结果(状态值)返回到ZDO_JoinConfirmCB()回调函数中。……NLME_JoinRequest ()
此函数允许相邻的更高层请求设备将自己加入到一个网络中。此函数的结果(状态)返回到ZDO_JoinConfirmCB()回调函数中。……NLME_ReJoinRequest ()
使用此函数重新加入一个设备已经加入过的网络。此函数的结果(状态)返回到ZDO_JoinConfirmCB()回调函数中。……
ZDO_JoinConfirmCB()一样只做了一件事,就是向ZDApp_event_loop发送事件ZDO_NWK_JOIN_IND。
下面是ZDO_NWK_JOIN_IND事件的处理函数ZDApp_ProcessNetworkJoin(已简化):
void ZDApp_ProcessNetworkJoin( void )
{
if ( (devState == DEV_NWK_JOINING) ||
((devState == DEV_NWK_ORPHAN) &&
(ZDO_Config_Node_Descriptor.LogicalType == NODETYPE_ROUTER)) )
{
// Result of a Join attempt by this device.
if ( nwkStatus == ZSuccess )
{
osal_set_event( ZDAppTaskID, ZDO_STATE_CHANGE_EVT );
if ( devState == DEV_NWK_JOINING )
{
ZDApp_AnnounceNewAddress();
}
devState = DEV_END_DEVICE;
}
else
{
if ( (devStartMode == MODE_RESUME) &&
(++retryCnt >= MAX_RESUME_RETRY) )
{
if ( _NIB.nwkPanId == 0xFFFF || _NIB.nwkPanId == INVALID_PAN_ID )
devStartMode = MODE_JOIN;
else
{
devStartMode = MODE_REJOIN;
_tmpRejoinState = true;
}
}
/******************************/
/*some process*/
/******************************/
zdoDiscCounter = 1;
ZDApp_NetworkInit( …… );
}
}
else if ( devState == DEV_NWK_ORPHAN || devState == DEV_NWK_REJOIN )
{
// results of an orphaning attempt by this device
if (nwkStatus == ZSuccess)
{
devState = DEV_END_DEVICE;
osal_set_event( ZDAppTaskID, ZDO_STATE_CHANGE_EVT );
ZDApp_AnnounceNewAddress();
}
else
{
if ( devStartMode == MODE_RESUME )
{
if ( ++retryCnt <= MAX_RESUME_RETRY )
{
if ( _NIB.nwkPanId == 0xFFFF || _NIB.nwkPanId == INVALID_PAN_ID )
devStartMode = MODE_JOIN;
else
{
devStartMode = MODE_REJOIN;
_tmpRejoinState = true;
}
}
// Do a normal join to the network after certain times of rejoin retries
else if( AIB_apsUseInsecureJoin == true )
{
devStartMode = MODE_JOIN;
}
}
// Clear the neighbor Table and network discovery tables.
nwkNeighborInitTable();
NLME_NwkDiscTerm();
// setup a retry for later...
ZDApp_NetworkInit( …… );
}
}
}
至此终端就完成了入网的全部流程,如果被父节点接受,那么入网成功;如果失败,则重新开始入网流程。
4. 提出问题
可以看出,函数中没有对失败时的Join方式或Rejoin方式做任何的处理,毫无疑问,两种方式下都将无限重试直到入网成功。并没有实现所谓的:
// Do a normal join to the network after certain times of rejoin retries
那么分析Orphan Join,而根据源代码的逻辑,如果是路由器(NODETYPE_ROUTER)执行Orphan Join,那么当重试次数超过MAX_RESUME_RETRY时,将根据是否搜索到了父节点(_NIB.nwkPanId == 0xFFFF || _NIB.nwkPanId == INVALID_PAN_ID),将入网方式重置为Join方式或Rejoin方式。那么针对Rejoin方式和终端(NODETYPE_DEVICE)的Orphan Join方式呢,很令人费解:
if ( devStartMode == MODE_RESUME )
{
if ( ++retryCnt <= MAX_RESUME_RETRY )
{
if ( _NIB.nwkPanId == 0xFFFF || _NIB.nwkPanId == INVALID_PAN_ID )
devStartMode = MODE_JOIN;
else
{
devStartMode = MODE_REJOIN;
_tmpRejoinState = true;
}
}
// Do a normal join to the network after certain times of rejoin retries
else if( AIB_apsUseInsecureJoin == true )
{
devStartMode = MODE_JOIN;
}
}
不管怎样,失败的Orphan Join都将直接被置为Join或Rejoin,在这里条件 (++retryCnt <= MAX_RESUME_RETRY)好像总是成立的。那么有没有可能是其他地方对retryCnt进行了修改,搜索遍整个工程,除了这个函数中有对retryCnt的+操作外,只有两处地方对retryCnt进行了赋值,一处是定义时的初始化,一处是断网重连,执行Orphan Join前对retryCnt的清零。
所以,对于终端来说,都只能执行一次Orphan Join,与宏定义MAX_RESUME_RETRY毫无关系。
这到底是TI有意为之,还是逻辑的Bug呢?这个问题有待日后解决。
【转】ZigBee终端入网方式深入分析的更多相关文章
- ZigBee设备入网流程之关联方式
ZigBee设备入网流程 ZigBee设备入网有关联方式和直接方式两种,我所熟悉的是关联方式,这也是最常用的方式. 关联方式 step1 设备发出Beacon Request 设备会在预先设置的几个信 ...
- 【转】zigbee终端无法重连的问题解决
zigbee终端无法重连的问题解决 1.zigbee重连的原因 (1)zigbee由于各种原因的干扰导致信号太差而掉线. (2)协调器重启. 2.zigbee终端重连的处理 (1)zigbee掉线后会 ...
- LoRaWAN协议(五)--OTAA入网方式详述
前言 OTAA(Over-The-Air Activation),是LoRaWAN的一种空中入网方式.当node在上电的时候处于非入网状态时,需要先入网才能和服务器进行通信.其操作就是node发送jo ...
- LoRaWAN协议(四)--入网方式概述
前言 在LoRaWAN中,node最终和服务器能够正常数据交互,需要先入网,入网的本质,也就是获得一些通信相关的参数,有以下几个: NwkSKey AppSKey DevAddr DevEui 其中 ...
- ubuntu-12.10-server中打开终端的方式
ubuntu-12.10-server系统在图形界面的任务栏上找不到终端的踪影,可以使用以下两种方式调出 1.在图形界面中点击Dash Home 点击后搜索terminal即可 2.可以通过快捷键CT ...
- linux shell终端打开方式
前言 Linux操作系统没有Window操作系统界面友好,使用者需要使用命令与系统进行交互,交互媒介为shell终端. 有三种方式可以打开终端: 方法一: 打开新的窗口并打开shell终端,快捷键:c ...
- 记录一个终端入网小助手的bug
背景:技术leader拿到一台超薄笔记本,系统标准化安装,笔记本一开机风扇嗡嗡响,键盘也开始发烫,资源占用排名前三的进程都是终端管理软件,一下子就找上门了.处理:进程分析发现异常,卸载入网小助手后恢复 ...
- ZigBee 入网详解
本文将根据Sniffer来详细解释ZigBee终端设备入网的整个流程,原创博文. 当协调器建立好网络后,终端设备执行zb_startrequest函数,准备入网时,他们两者之间详细的流程如下.
- TI Zigbee Light Link 参考设计
TI Zigbee Light Link 参考设计 原文出处: http://processors.wiki.ti.com/index.php/Category:ZigBee_Light_Link ...
随机推荐
- 玩转Vim 编辑器
一:VIM快速入门 1.vim模式介绍 以下介绍内容来自维基百科Vim 从vi演生出来的Vim具有多种模式,这种独特的设计容易使初学者产生混淆.几乎所有的编辑器都会有插入和执行命令两种模式,并且大多数 ...
- Collection集合
一些关于集合内部算法可以查阅这篇文章<容器类总结>. (Abstract+) Collection 子类:List,Queue,Set 增: add(E):boolean addAll(C ...
- 太多选择——企业如何选择合适的BI工具?
在没认清现状前,企业当然不能一言不合就上BI. BI不同于一般的企业管理软件,不能简单归类为类似用于提高管理的ERP和WMS,或用于提高企业效率的OA.BPM.BI的本质应该是通过展现数据,用于加强企 ...
- HttpPost过程中使用的URLEncoder.encode(something, encode)
URLEncoder.encode("刘美美", "utf-8").toString() = %E5%88%98%E7%BE%8E%E7%B ...
- ionic第一坑——ion-slide-box坑(ion-slide分两页的坑)
ionic.views.Slider = ionic.views.View.inherit({ initialize: function (options) { . . . function setu ...
- Windows下Redis缓存服务器的使用 .NET StackExchange.Redis Redis Desktop Manager
Redis缓存服务器是一款key/value数据库,读110000次/s,写81000次/s,因为是内存操作所以速度飞快,常见用法是存用户token.短信验证码等 官网显示Redis本身并没有Wind ...
- 两个变量交换的四种方法(Java)
对于两种变量的交换,我发现四种方法,下面我用Java来演示一下. 1.利用第三个变量交换数值,简单的方法. (代码演示一下) class TestEV //创建一个类 { public static ...
- Linux的学习笔记
Linux,1991年,系统安全,良好的可移植性,多用户,多任务,良好的兼容性,良好的用户界面, 主流的是RedHat或者CentOS, CentOS 设置的网关 192.168.2.2 Window ...
- keepalive的不足,如何处理
MySQL(或者其它服务)的keepalived高可用监控脚本 开发脚本需求:我们知道,keepalive是基于虚拟ip的存活来判断是否抢占master的机制的,但是如果我们做了MySQL的keepa ...
- AutoMapper(六)
返回总目录 List和数组 AutoMapper只要求元素类型的配置而不要求可能会用到的任何数组或者list类型.比如,我们有一个简单的源和目标类型: public class Source{ pub ...