1. 模块概述
dnetwork 是整个机器人系统(ZJUDancer)的网络通信中枢。
- 对内(ROS 节点): 它是各个本地算法模块(视觉 Vision、动作 Motion、行为 Behavior)的信息聚合器和分发器。
- 对外(UDP 网络): 它是机器人与外界(队友机器人、裁判盒、外部监控端)进行物理数据包交换的唯一接口。
该模块高度依赖底层的两个基础库:
dprocess: 提供多线程和定时循环调度的能力。dtransmit: 提供对底层 UDP Socket 的高度封装(异步接收、回调绑定、组播/单播发送)。
2. 目录结构与核心入口
dnetwork 的核心文件包含头文件与源文件两部分,核心依赖标准 RoboCup 的协议头文件:
dnetwork/
├── include/dnetwork/
│ ├── RoboCup/RoboCupGameControlData.h # 国际 RoboCup 官方定义的裁判盒数据结构
│ ├── gamecontroller.hpp # 裁判盒通信类头文件
│ └── team.hpp # 队内通信类头文件
└── src/
├── gamecontroller.cpp
├── main.cpp # ROS 节点入口
└── team.cpp
2.1 模块启动 (main.cpp)
作为独立的 ROS 节点,程序入口非常简洁。初始化 ROS 后,实例化 Team 和 GameController。由于它们均继承自 dprocess::DProcess,调用 spin() 后会分别在独立的子线程中启动网络监听和定时发送的循环,最后主线程通过 join() 阻塞等待。
3. Team 类:队内通信机制详解
Team 类的主要职责是实现多台机器人之间的状态同步(队友在哪、是否看到球、当前处于什么战术角色),并将本机的状态暴露给外部监控。其运行频率设定为 NETWORK_FREQ = 30Hz。
3.1 接收数据(从队友到本机)
在构造函数中,通过 dtransmit 的 addRawRecv 绑定了对广播地址 dconstant::network::TeamInfoBroadcastAddress 的监听。
处理逻辑(回调函数):
- 尺寸校验: 检查收到的数据包大小是否严格等于
dmsgs::TeamInfo结构体的大小。 - 阵营过滤: 解析数据包,判断
team_number是否与本机所属队伍相符(过滤掉场上敌方机器人的干扰包)。 - ROS 转发: 符合条件的队友消息,会被打上当前时间戳
recv_timestamp,并通过/dnetwork_n/TeamInfo发布到本地 ROS 总线供 Behavior 等决策模块使用。
3.2 收集本机状态(ROS 订阅)
Team 类在本地订阅了三个核心话题,利用回调函数(带有 std::mutex 线程锁保证数据安全)更新内部的 info_ 结构体:
- MotionInfo: 获取机器人是否处于稳定状态 (
unstable_)。如果摔倒,则标记为失去比赛能力 (incapacitated = true)。 - BehaviorInfo: 获取机器人的当前角色(前锋/后卫等)、战术状态、目标点。特别注意: 这里对
voronoi(维诺图) 数据做了截断处理,限制最大长度为 6 (MAX_VORONOI_SIZE),防止数组越界。 - VisionInfo: 获取机器人当前视野信息(是否看到球/球门、球的全局/相对坐标)。
- GCInfo: 从本地获取经过
GameController模块解析好的裁判盒状态(是否被罚下)。
3.3 发送数据与动态频率控制 (tick() 函数)
这是 Team 类的核心亮点。由于网络带宽有限,程序实现了动态发包频率控制 (Dynamic Frequency Control):
- 高频 (15Hz / 0.067s): 如果本机是持球机器人 (
BALL_HANDLING),需要将球的位置极速同步给队友。 - 中高频 (10Hz ~ 2.5Hz): 如果本机看到了球,根据球距离机器人的远近决定发包频率。距离越近(<1m),发包越快(10Hz);距离远(>3m),发包降为 2.5Hz。
- 低频 (1Hz / 1.0s): 如果没有看到球,且不持球,处于“盲人”状态,仅维持 1Hz 的心跳包。
发送通道:
- 局域网广播 (Broadcast): 通过
transmitter_->sendRaw将info_广播给所有队友。发送前提是behaviorReady_必须为 true(确保有最新决策数据)。 - UDP 单播监控 (Unicast Monitor): 通过原生的 Socket 编程(
socket(),sendto()),以固定的 1.0s 周期,将info_单播发送到配置文件中指定的UnicastTargetAddress和UnicastTargetPort,主要用于场外监控。
4. GameController 类:裁判盒通信机制详解
GameController 负责与比赛官方的裁判盒系统(GameController Server)对接。运行频率较低,为 FREQ = 2Hz。
4.1 接收数据(从裁判盒到本机)
在构造函数中,通过 dtransmit 的 addRawRecvFiltered 绑定监听端口 3838(GAMECONTROLLER_DATA_PORT)。过滤条件更严格,仅处理大小为 sizeof(RoboCupGameControlData) 的包,并在拿到锁后调用 ParseData()。
IsValidData() 数据有效性严格校验:
- Header 校验: 前 4 个字节必须是
"RGme"。 - 比赛归属校验: 检查数据包中包含的两个队伍的
teamNumber,其中必须有一个是本机的队伍编号。 - 如果一切通过,更新
last_valid_packet_timestamp_。
4.2 数据解析与转换 (tick() 函数)
裁判盒发来的原始数据 (RoboCupGameControlData) 结构非常庞大。tick() 函数将其剥离并转化为内部紧凑的 dmsgs::GCInfo:
- 连接状态监控: 通过判断当前时间与
last_valid_packet_timestamp_的差值。如果超过 3 秒没有收到合法数据包,判定为断开连接 (connected_ = false)。 - 阵营判定: 根据
teamNumber_自动区分解析包里的ourTeam(己方)和enemyTeam(敌方)。 - 复杂比赛状态拆解:
- 将
gameData.state拆解为针对当前阶段的setPlayReady(允许走位) 和setPlayFreeze(必须静止)。 - 判断具体的定位球类型(任意球、点球、角球、球门球、界外球),并精确区分是己方的球权还是敌方的球权 (
ourDirectFreeKick,enemyDirectFreeKick等)。
- 将
- 惩罚状态提取: 读取
ourTeam->players[playerNumber_ - 1].penalty,判断本机当前是否处于判罚/罚下状态 (penalised_)。 - 最后,将整理好的
info_通过/dnetwork_n/GCInfo发布给本地 ROS 其他节点。
4.3 发送数据返回裁判盒 (tick() 函数尾部)
按照 RoboCup 规则,机器人需要向裁判盒报告自己的状态(存活心跳、机器人位置、球的相对位置)。
目前代码构建了 RoboCupGameControlReturnData ret_ 结构体:
- 发送目标: 向配置参数
gameControllerAddress_的 3939 端口 (GAMECONTROLLER_RETURN_PORT) 回传数据。 - 底层实现: 注意,这里因为裁判盒要求单播,代码没有使用
transmitter_->sendRaw,转而直接使用了底层的 Linux Socket API 创建 UDP Socket 发送 (socket,inet_pton,sendto,close)。
5. 模块数据流向图总结
以下两个闭环可以帮助理解 dnetwork:
A. 队友协作闭环 (端口 57335):
[本地Vision/Behavior/Motion] --(ROS)--> [Team::tick] --(UDP 广播 57335)--> 局域网
局域网 --(UDP 广播 57335)--> [Team::addRawRecv] --(ROS)--> [本地决策系统]
B. 裁判盒调度闭环 (收 3838,发 3939):
裁判盒 --(UDP 广播 3838)--> [GC::addRawRecvFiltered] --> [GC::ParseData] --(ROS)--> [本地系统]
[GC::tick生成回传数据 ret_] --(UDP 单播 3939)--> 裁判盒