• 【转】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。

    1. Join入网过程。首先发起Network Discovery,返回所有应答的节点的信息。在发现的结果中找出符合要求(这里的要求是一些最基本的条件,详见ZigBee协议规范3.6.1.4.1.1节,下同)的父节点,向它发送入网请求。父节点分配16位网络地址。

    2. Rejoin入网过程。发起Network Discovery,在应答的节点中挑选出和自己的ExtendedPANID相同的节点,在这些节点中找出符合要求的父节点,发送入网请求,并且使用自己已拥有的16位网络地址(若没有,则随机生成一个)。

    3. 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呢?这个问题有待日后解决。

  • 相关阅读:
    Ocelot简易教程(二)之快速开始1
    Ocelot简易教程(一)之Ocelot是什么
    InfluxDB学习之InfluxDB的基本操作
    InfluxDB入门教程
    .NET Core微服务之基于App.Metrics+InfluxDB+Grafana实现统一性能监控
    .net Core 微服务
    IdentityServer4 接口说明
    WINDOWS命令行关闭本地占用的端口
    并发负载压力测试
    C#操作Mongodb
  • 原文地址:https://www.cnblogs.com/yelin/p/6054611.html
Copyright © 2020-2023  润新知