EIP之数据发送

一、初始化

设置默认值
在初始化协议栈之前,需要确定设备的网络配置,包括 IP 地址、子网掩码、网关等信息。这些信息可以通过读取配置文件来获取,也可以由用户在程序中直接设置。如果没有配置文件或者用户没有提供网络配置信息,则可以使用默认值进行初始化。在 OpENer 中,可以使用 SetDeviceIpAddress、SetDeviceSubnetMask、SetDeviceGatewayAddress 等函数设置设备的网络配置信息。

初始化 CIP 层
在设备的 EtherNet/IP 实现中,CIP(Control and Information Protocol)层是 EtherNet/IP 协议栈中最核心的部分。 CIP 层处理 EtherNet/IP 消息的编码、解码、封装和解封装等任务。在 OpENer 中,可以使用 CipStackInit 函数初始化 CIP 层的对象,并设置默认的属性值。

设置 MAC 地址
MAC(Media Access Control)地址是设备的硬件地址,用于标识设备在网络中的唯一性。在 OpENer 中,可以使用 CipEthernetLinkSetMac 函数设置设备的 MAC 地址。

加载 NV 数据
NV(Non-Volatile)数据是指存储在设备的非易失性存储器(如 Flash 存储器)中的数据,通常包括设备的 IP 地址、子网掩码、网关等信息。在 OpENer 中,可以使用 NvdataLoad 函数从非易失性存储器中加载设备的 NV 数据,如果 NV 数据不存在,则使用默认值进行初始化。

网络接口初始化
在初始化过程中,需要初始化设备的网络接口,包括设置 IP 地址、子网掩码、网关等信息。在 OpENer 中,可以使用 BringupNetwork 函数初始化设备的网络接口,如果设备的网络配置是动态获取的,则可以启动 DHCP 客户端获取网络配置信息。

注册信号处理函数
在 OpENer 中,可以使用 signal 函数注册信号处理函数,如 LeaveStack 函数,用于处理程序终止信号(如 Ctrl-C)。当程序收到终止信号时,会调用 LeaveStack 函数进行清理工作,并退出程序。

执行事件循环
在初始化完成后,程序进入事件循环,处理接收到的 EtherNet/IP 消息和连接请求。在 OpENer 中,可以使用 executeEventLoop 函数进入事件循环,该函数内部会重复调用 NetworkHandlerProcessCyclic 函数来处理接收到的消息。

以上就是 EtherNet/IP 协议栈的初始化过程,其中还有一些细节处理(如错误处理、日志输出等),具体实现可能会有所差异。

1.初始化双向链表,储存连接对象
DoublyLinkedListInitialize(&connection_list, 
                             CipConnectionObjectListArrayAllocator,
                             CipConnectionObjectListArrayFree);

“Class 1” 连接对象:用于在本地网络上连接设备,例如通过以太网连接。
“Class 2” 连接对象:用于在远程网络上连接设备,例如通过因特网连接。
“Class 3” 连接对象:用于在 CIP 设备之间直接连接,例如通过控制器之间的串行连接。
“Class 0” 连接对象:用于在控制器和设备之间建立直接连接,并允许通过 CIP 进行通信。

2. 获取MAC地址,测试网络接口环境
EipStatus IfaceGetMacAddress(const char *iface,
                             uint8_t *const physical_address)
3. 设置设备序列号
SetDeviceSerialNumber(123456789);
4. cip协议栈初始化
  EipUint16 unique_connection_id = rand();
  EipStatus eip_status = CipStackInit(unique_connection_id);

CipStackInit 这是一个函数定义,用于初始化 EtherNet/IP 协议栈。函数的输入参数是一个唯一连接 ID。
该函数的主要作用是按顺序初始化协议栈中的各个模块。具体来说,它执行以下操作:

  • 调用 CipMessageRouterInit 函数,初始化消息路由器对象。
  • 调用 CipIdentityInit 函数,初始化身份识别对象。
  • 调用 CipTcpIpInterfaceInit 函数,初始化 TCP/IP 接口对象。
  • 调用 CipEthernetLinkInit 函数,初始化以太网连接对象。
  • 调用 ConnectionManagerInit 函数,初始化连接管理器对象。
  • 调用 CipAssemblyInitialize 函数,初始化数据组装对象。
  • 如果编译器选项 OPENER_IS_DLR_DEVICE 定义为非零值,则调用 CipDlrInit 函数,初始化 DLR 对象。
  • 调用 CipQoSInit 函数,初始化 QoS 对象。
  • 如果编译器选项 CIP_FILE_OBJECT 定义为非零值,则调用 CipFileInit 函数,初始化文件对象。
  • 如果编译器选项 CIP_SECURITY_OBJECTS 定义为非零值,则调用 CipSecurityInit 函数,初始化安全对象;调用 EIPSecurityInit 函数,初始化 EIP 安全对象;调用 CertificateManagementObjectInit 函数,初始化证书管理对象。
  • 调用 ApplicationInitialization 函数,执行应用程序初始化。

函数的返回值是 EtherNet/IP 协议栈的初始化状态。

二、执行事件循环

执行executeEventLoop进入事件循环
1. 等待可读的文件描述符

首先,程序使用 select 函数等待可读的文件描述符,包括 TCP 监听套接字、UDP 单播套接字、UDP 全局广播套接字和消费者 UDP 套接字。在 OpENer 中,这些套接字都被注册到了一个文件描述符集合中,使用 read_socket 变量保存这个文件描述符集合。此外,由于 EtherNet/IP 协议栈需要保持一个固定的时间间隔来处理事件,因此需要计算出 select 函数的超时时间,即 g_time_value 变量。

2. 处理接收到的消息和连接请求

select 函数返回后,程序会遍历已经准备好的套接字,并根据套接字类型调用相应的函数来处理接收到的消息和连接请求。具体来说:

  • 对于 TCP 监听套接字,调用 CheckAndHandleTcpListenerSocket 函数来处理连接请求,如果有新的连接请求,则会创建一个新的 TCP 连接,并将连接信息保存到连接管理器中。

  • 对于 UDP 单播套接字,调用 CheckAndHandleUdpUnicastSocket 函数来处理接收到的 UDP 数据报,如果有新的数据报,则会调用 HandleReceivedExplictUdpData 函数来处理数据报。

  • 对于 UDP 全局广播套接字,调用 CheckAndHandleUdpGlobalBroadcastSocket 函数来处理接收到的 UDP 数据报,如果有新的数据报,则会调用 HandleReceivedBroadcastUdpData 函数来处理数据报。

  • 对于消费者 UDP 套接字,调用 CheckAndHandleConsumingUdpSocket 函数来处理接收到的 UDP 数据报,如果有新的数据报,则会调用 HandleReceivedIoConnectionData 函数来处理数据报。

3. 处理已连接的 TCP 套接字

对于每个已连接的 TCP 套接字,程序会调用 HandleDataOnTcpSocket 函数来处理接收到的数据流。该函数会从套接字缓冲区中读取数据,并根据数据类型调用相应的处理函数,如 HandleReceivedData 函数来处理 CIP 数据,或者 HandleSentData 函数来处理发送完成的数据。

4. 关闭不活动的连接

程序会调用 CheckEncapsulationInactivity 函数来检查连接是否已经不活动,如果是,则关闭连接,并从连接管理器中删除该连接。

5. 处理连接管理器中的连接

程序会调用 ManageConnections 函数来处理连接管理器中的连接,包括检查连接是否超时、发送心跳包等操作。

6. 调用超时检查函数

程序会遍历 timeout_checker_array 中注册的超时检查函数,并依次调用这些函数。这些函数可以用于检查 CIP 状态机是否超时,或者检查其他组件是否超时等。

7. 更新网络状态中的计时器和时间戳

最后,程序会更新网络状态中的计时器和时间戳,以便下一次事件循环时使用。具体来说,程序会计算出当前时间和上一次事件处理时间的时间差,累加到 g_network_status.elapsed_time 变量中,同时更新 g_last_time 变量的值。如果 g_network_status.elapsed_time 的值大于等于 kOpenerTimerTickInMilliSeconds,则说明已经过了一个固定的时间间隔,需要调用 ManageConnections 函数处理连接管理器中的连接,并执行 timeout_checker_array 中注册的超时检查函数。最后,将 g_network_status.elapsed_time 的值清零,以便下一次计时。

三、管理连接

执行 ManageConnections 函数来处理连接管理器中的连接

1. 通知应用程序可以执行

首先,程序会调用 HandleApplication 函数,通知应用程序可以执行。这个函数的作用是检查应用程序是否有可执行的任务,如果有,则调用应用程序注册的回调函数来执行任务。这个函数在 OpENer 中是一个空函数,需要用户自己来实现。

2. 处理封装消息

接下来,程序会调用 ManageEncapsulationMessages 函数,处理封装消息。该函数的作用是从接收缓冲区中读取封装消息,并根据消息类型调用相应的处理函数来处理消息。该函数中使用了一个全局的 g_delayed_encapsulation_messages 数组来存储所有延迟的封装消息。在遍历该数组时,对于每个已经设置了 socket 的消息,都会更新其超时计时器,并检查该计时器是否已经到达或超过了设定的时间。如果已经到达或超过了设定的时间,则调用 sendto 函数将该消息发送出去,并将该消息的 socket 设置为无效值,表示该消息已经被发送。需要注意的是,该函数并不会主动发送封装消息,而是在调用该函数时,检查是否有已经设置了 socket 的延迟消息需要发送。
3. 遍历连接列表

接下来,程序会遍历连接列表中的所有连接,并对每个连接执行以下操作:

  • 如果连接状态为已建立,则检查连接是否需要发送数据。如果需要发送数据,则根据连接的传输类别和触发方式来决定是否需要发送数据,以及发送数据的时间间隔。具体来说:

    • 如果连接的传输类别为循环触发,则每隔指定的时间间隔发送一次数据,即 ConnectionObjectGetRequestedPacketInterval 函数返回的值。

    • 如果连接的传输类别为基于触发的,则根据触发条件和触发方式来决定是否需要发送数据。

  • 如果连接状态为已建立,并且连接是消费者连接或服务器端连接,则检查连接的不活动监视器是否已经超时,如果超时,则关闭连接,并调用连接超时函数来通知应用程序连接已经关闭。具体来说,程序会检查连接的不活动监视器是否已经超过指定的时间间隔,即 connection_object->inactivity_watchdog_timer 变量的值。如果超时,则调用 connection_object->connection_timeout_function 函数,通知应用程序连接已经关闭。

  • 如果连接状态为已建立,则更新连接的不活动监视器和最后包监视器的计时器。具体来说,程序会将 elapsed_time 变量的值减去连接的不活动监视器计时器和最后包监视器计时器的值,以更新它们的值。

  • 如果连接状态为已建立,并且连接是生产者连接,则检查生产抑制计时器是否已经过期,如果过期,则将计时器重置,并允许连接发送数据。具体来说,程序会检查连接的生产抑制计时器是否已经超过指定的时间间隔,即 connection_object->production_inhibit_timer 变量的值。如果超时,则调用 ConnectionObjectResetProductionInhibitTimer 函数来重置计时器,并允许连接发送数据。

4. 更新计时器

最后,程序会更新连接中的传输触发器计时器和生产者抑制计时器,以便下一次事件循环时使用。具体来说,程序会将连接的传输触发器计时器减去 elapsed_time 变量的值,以更新它们的值。

需要注意的是,该函数并不会主动发送数据,而是等待下一次事件循环时再发送数据。此外,该函数仅处理连接管理器中的连接,而不处理 TCP 监听套接字、UDP 单播套接字、UDP 全局广播套接字和消费者 UDP 套接字。

EipStatus ManageConnections(MilliSeconds elapsed_time) {
  //OPENER_TRACE_INFO("Entering ManageConnections\n");

  /* 通知应用程序可以执行 */
  HandleApplication();

  /* 处理封装消息 */
  ManageEncapsulationMessages(elapsed_time);

  /* 遍历连接列表,对于每个连接 */
  DoublyLinkedListNode *node = connection_list.first;
  while(NULL != node) {
    //OPENER_TRACE_INFO("Entering Connection Object loop\n");

    /* 获取连接对象 */
    CipConnectionObject *connection_object = node->data;

    /* 如果连接状态为已建立,则继续执行以下步骤 */
    if(kConnectionObjectStateEstablished == ConnectionObjectGetState(connection_object) ) {

      /* 如果连接是消费者连接或服务器端连接,则检查连接的不活动监视器是否已经超时 */
      if( (NULL != connection_object->consuming_instance) || /* we have a consuming connection check inactivity watchdog timer */
          (kConnectionObjectTransportClassTriggerDirectionServer == ConnectionObjectGetTransportClassTriggerDirection(connection_object) ) ) /* all server connections have to maintain an inactivity watchdog timer */
      {
        if(elapsed_time >= connection_object->inactivity_watchdog_timer) {
          /* 如果超时,则关闭连接,并调用连接超时函数来通知应用程序连接已经关闭 */
          OPENER_TRACE_INFO(">>>>>>>>>>Connection ConnNr: %u timed out\n", connection_object->connection_serial_number);
          OPENER_ASSERT(NULL != connection_object->connection_timeout_function);
          connection_object->connection_timeout_function(connection_object);
        } else {
          /* 如果没有超时,则更新连接的不活动监视器和最后包监视器的计时器 */
          connection_object->inactivity_watchdog_timer -= elapsed_time;
          connection_object->last_package_watchdog_timer -= elapsed_time;
        }
      }

      /* 只有当连接没有超时时,才检查是否需要发送数据 */
      if(kConnectionObjectStateEstablished == ConnectionObjectGetState(connection_object) ) {
        /* 如果连接是生产者连接,则检查生产抑制计时器是否已经过期 */
        if( (0 != ConnectionObjectGetExpectedPacketRate(connection_object) ) && (kEipInvalidSocket != connection_object->socket[kUdpCommuncationDirectionProducing]) ) /* only produce for the master connection */
        {
          if(kConnectionObjectTransportClassTriggerProductionTriggerCyclic != ConnectionObjectGetTransportClassTriggerProductionTrigger(connection_object) ) {
            /* 如果连接的传输类别为非循环触发,则检查生产抑制计时器是否已经过期 */
            if(elapsed_time <= connection_object->production_inhibit_timer) {
              /* 如果生产抑制计时器没有过期,则不允许连接发送数据 */
              //The connection is allowed to send again
            } else {
              /* 如果生产抑制计时器已经过期,则允许连接发送数据,并重置生产抑制计时器 */
              connection_object->production_inhibit_timer -= elapsed_time;
            }
          }

          /* 如果连接的传输类别为循环触发或触发条件已满足,则发送数据 */
          if(connection_object->transmission_trigger_timer <= elapsed_time) { /* need to send package */
            /* 调用连接的发送数据函数,发送数据 */
            OPENER_ASSERT(NULL != connection_object->connection_send_data_function);
            EipStatus eip_status = connection_object->connection_send_data_function(connection_object);
            if(eip_status == kEipStatusError) {
              OPENER_TRACE_ERR("sending of UDP data in manage Connection failed\n");
            }

            /* 计算下一次发送数据的时间点,并更新传输触发器计时器 */
            connection_object->transmission_trigger_timer += ConnectionObjectGetRequestedPacketInterval(connection_object);
            /* 如果 elapsed_time 小于传输触发器计时器的值,则将传输触发器计时器减去 elapsed_time */
            if (connection_object->transmission_trigger_timer > elapsed_time) {
              connection_object->transmission_trigger_timer -= elapsed_time;
            } else {  /* 如果 elapsed_time 大于传输触发器计时器的值,则将传输触发器计时器重置为 0 */
              connection_object->transmission_trigger_timer = 0;
              OPENER_TRACE_INFO("elapsed time: %lu ms was longer than RPI: %u ms\n", elapsed_time, ConnectionObjectGetRequestedPacketInterval(connection_object));
            }

            /* 如果连接的传输类别为非循环触发,则重置生产抑制计时器 */
            if(kConnectionObjectTransportClassTriggerProductionTriggerCyclic != ConnectionObjectGetTransportClassTriggerProductionTrigger(connection_object) ) {
              ConnectionObjectResetProductionInhibitTimer(connection_object);
            }
          } else {
            /* 如果还不需要发送数据,则更新传输触发器计时器 */
            connection_object->transmission_trigger_timer -= elapsed_time;
          }
        }
      }
    }

    /* 处理下一个连接 */
    node = node->next;
  }

  /* 返回处理连接的结果 */
  return kEipStatusOk;
}

四、发送数据

endConnectedData该函数的主要工作是组装封装数据包,然后通过调用 SendUdpData 函数来将其发送出去。在组装封装数据包的过程中,需要先增加 EIP 序列号,然后设置通用数据包格式中的地址项和数据项。如果连接对象的传输级别不是 Connection Class 0,则需要使用序列化地址项。接着,将连接对象中的数据拷贝到封装数据包中,并设置数据项的长度。如果数据项的长度为零,则说明该封装数据包是一个心跳包。如果需要在数据项中添加一个 4 字节的 Run/Idle 状态,也需要在此处进行处理。最后,调用 SendUdpData 函数将封装数据包发送出去。

EipStatus SendConnectedData(CipConnectionObject *connection_object) {
  /* 获取通用数据包格式 */
  CipCommonPacketFormatData *common_packet_format_data =
    &g_common_packet_format_data_item;

  /* 增加 EIP 序列号 */
  connection_object->eip_level_sequence_count_producing++;

  /* 组装通用数据包格式 */
  common_packet_format_data->item_count = 2;
  if(kConnectionObjectTransportClassTriggerTransportClass0 !=
     ConnectionObjectGetTransportClassTriggerTransportClass(connection_object) )
  /* 如果不是 Connection Class 0,则使用序列化地址项 */
  {
    common_packet_format_data->address_item.type_id =
      kCipItemIdSequencedAddressItem;
    common_packet_format_data->address_item.length = 8;
    common_packet_format_data->address_item.data.sequence_number =
      connection_object->eip_level_sequence_count_producing;
  } else {
    common_packet_format_data->address_item.type_id =
      kCipItemIdConnectionAddress;
    common_packet_format_data->address_item.length = 4;
  }
  common_packet_format_data->address_item.data.connection_identifier =
    connection_object->cip_produced_connection_id;

  common_packet_format_data->data_item.type_id = kCipItemIdConnectedDataItem;

  CipByteArray *producing_instance_attributes =
    (CipByteArray *) connection_object->producing_instance->attributes->data;
  common_packet_format_data->data_item.length = 0;

  /* 通知应用程序数据将在调用后立即发送 */
  if(BeforeAssemblyDataSend(connection_object->producing_instance) ) {
    connection_object->sequence_count_producing++;
  }

  /* 将地址信息项设置为无效类型 */
  common_packet_format_data->address_info_item[0].type_id = 0;
  common_packet_format_data->address_info_item[1].type_id = 0;

  /* 组装 I/O 消息 */
  ENIPMessage outgoing_message;
  InitializeENIPMessage(&outgoing_message);
  AssembleIOMessage(common_packet_format_data, &outgoing_message);

  MoveMessageNOctets(-2, &outgoing_message);
  common_packet_format_data->data_item.length =
    producing_instance_attributes->length;

  bool is_heartbeat = (common_packet_format_data->data_item.length == 0);
  if(s_produce_run_idle && !is_heartbeat) {
    common_packet_format_data->data_item.length += 4;
  }

  if(kConnectionObjectTransportClassTriggerTransportClass1 ==
     ConnectionObjectGetTransportClassTriggerTransportClass(connection_object) )
  {
    common_packet_format_data->data_item.length += 2;
    AddIntToMessage(common_packet_format_data->data_item.length,
                    &outgoing_message);
    AddIntToMessage(connection_object->sequence_count_producing,
                    &outgoing_message);
  } else {
    AddIntToMessage(common_packet_format_data->data_item.length,
                    &outgoing_message);
  }

  if(s_produce_run_idle && !is_heartbeat) {
    AddDintToMessage( g_run_idle_state,
                      &outgoing_message );
  }

  memcpy(outgoing_message.current_message_position,
         producing_instance_attributes->data,
         producing_instance_attributes->length);

  outgoing_message.current_message_position +=
    producing_instance_attributes->length;
  outgoing_message.used_message_length += producing_instance_attributes->length;

  /* 发送 UDP 数据 */
  return SendUdpData(&connection_object->remote_address,
                     &outgoing_message);
}

SendUdpData 函数用于将封装好的数据包通过 UDP 发送出去。以下是该函数的详细注释:

EipStatus SendUdpData(const struct sockaddr_in *const address,
                      const ENIPMessage *const outgoing_message) {
#if defined(OPENER_TRACE_ENABLED)
  static char ip_str[INET_ADDRSTRLEN];
  OPENER_TRACE_INFO(
      "UDP packet to be sent to: %s:%d\n",
      inet_ntop(AF_INET, &address->sin_addr, ip_str, sizeof ip_str),
      ntohs(address->sin_port));
#endif

  /* 调用 sendto 函数向指定地址发送数据 */
  int sent_length = sendto(g_network_status.udp_io_messaging,
                           (char *)outgoing_message->message_buffer,
                            outgoing_message->used_message_length, 0,
                            (struct sockaddr*) address, sizeof(*address));
  if(sent_length < 0) {
    /* 发送失败,记录错误信息 */
    int error_code = GetSocketErrorNumber();
    char *error_message = GetErrorMessage(error_code);
    OPENER_TRACE_ERR(
        "networkhandler: error with sendto in SendUDPData: %d - %s\n",
        error_code,
        error_message);
    FreeErrorMessage(error_message);
    return kEipStatusError;
  }

  if(sent_length != outgoing_message->used_message_length) {
    /* 发送数据长度与要发送的长度不一致,记录警告信息 */
    OPENER_TRACE_WARN(
      "data length sent_length mismatch; probably not all data was sent in SendUdpData, sent %d of %d\n",
      sent_length,
      outgoing_message->used_message_length);
    return kEipStatusError;
  }

  return kEipStatusOk;
}

该函数使用 sendto 函数将数据包通过 UDP 发送出去,并返回发送状态。如果发送失败,则记录错误信息并返回错误状态。如果发送的数据长度与要发送的长度不一致,则记录警告信息并返回错误状态。

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值