4. Zenoh协议入门

在PX4与ROS2的通信工程中,Zenoh并不是“另一个DDS”,而是一套面向边缘与嵌入式场景重新思考过的数据总线。它要解决的核心矛盾是:机器人系统既需要像DDS那样低延迟、去中心化的实时数据流,又需要像MQTT/HTTP那样能跨广域网、能穿透NAT、能按需查询历史与状态。Zenoh用一套统一的抽象把“发布订阅”“查询”“存储”三种交互模式整合到同一个键空间里,这正是它最值得先理解的设计哲学。

一、设计哲学:以键空间为中心的统一抽象

Zenoh的设计哲学可以概括为三点:数据即资源、位置透明、按需拉取与推送并存。所有可被访问的数据——无论是PX4发布的传感器话题、参数、日志,还是ROS2节点暴露的服务——都被映射为一个键表达式(Key Expression)。键表达式既是寻址方式,也是订阅过滤器,还是查询条件。这意味着发布者、订阅者、查询者、存储节点之间不需要预先约定接口类型,只需要对键空间达成一致。

位置透明体现在:应用只关心键,不关心数据在本地、局域网还是云端。Zenoh的路由层会自动完成发现、路由与转发。对于PX4这种算力受限的飞控,Zenoh提供了极轻量的客户端库,可以只订阅自己关心的键,避免全量话题带来的带宽与内存压力。

核心认知:Zenoh不是“消息队列”,而是“以键表达式为索引的分布式数据空间”。发布订阅、查询、存储都是这个空间上的操作,而不是三套独立协议。

二、发布订阅模型:数据流的主通道

发布订阅是Zenoh最常用的模式。发布者用put向某个键写入数据,订阅者用declare_subscriber声明对某个键表达式的兴趣。Zenoh的路由器或对等节点负责把数据从发布者转发到所有匹配的订阅者。与DDS不同,Zenoh的订阅是基于键表达式匹配的,而不是基于类型化主题;与MQTT不同,Zenoh支持真正的对等通信,不强制依赖中心Broker。

在PX4场景中,典型用法是:机载计算机上的Zenoh桥接节点把PX4的uORB话题(如sensor_combined、vehicle_attitude)映射为Zenoh键,例如px4/sensor/combined。地面站或ROS2节点订阅px4/sensor/**即可接收全部传感器数据。发布订阅的QoS可以通过congestion_control、priority、express等参数调节,以适应遥测链路带宽波动。

// Zenoh发布订阅示例(Rust风格伪代码)
let session = zenoh::open(config).await?;

// 发布者:向键写入数据
session.put("px4/vehicle/attitude", attitude_bytes).await?;

// 订阅者:声明对键表达式的兴趣
let subscriber = session
    .declare_subscriber("px4/vehicle/**")
    .await?;

while let Ok(sample) = subscriber.recv_async().await {
    println!("key={}, payload={:?}", sample.key_expr(), sample.payload());
}

三、键表达式:寻址、过滤与层级语义

键表达式是Zenoh的“地址语言”。它由斜杠分隔的层级组成,支持通配符:*匹配单层,**匹配任意多层。例如px4/sensor/*匹配px4/sensor/imu和px4/sensor/baro,但不匹配px4/sensor/imu/raw;而px4/sensor/**则匹配其下所有层级。键表达式还支持$*等更细粒度的通配,用于参数化查询。

键表达式的设计直接影响了系统可维护性。建议在PX4+ROS2工程中采用统一命名规范:px4/<模块>/<对象>/<属性>,ROS2侧则用ros2/<节点>/<话题>。这样订阅者可以用一个**表达式同时覆盖飞控与ROS2数据,而无需为每个话题单独配置。

键表达式 匹配示例 不匹配示例 典型用途
px4/sensor/* px4/sensor/imu px4/sensor/imu/raw 订阅单层传感器话题
px4/sensor/** px4/sensor/imu/raw px4/vehicle/attitude 订阅全部传感器子树
px4/**/attitude px4/vehicle/attitude px4/vehicle/attitude/rate 跨模块匹配姿态话题
ros2/*/cmd_vel ros2/offboard/cmd_vel ros2/offboard/cmd_vel/raw ROS2控制指令订阅

四、查询与存储机制:从“实时流”到“可回溯状态”

发布订阅解决的是“数据来了就推”,但机器人系统还需要“我现在想知道某个键的当前值”以及“过去一段时间的数据在哪里”。Zenoh用查询(Query)和存储(Storage)来补齐这两块。

查询通过get发起,携带一个键表达式,Zenoh会把查询路由到所有能回答该键的节点。存储节点(Storage)是订阅了某些键、并把最新值或历史值保存下来的Zenoh节点。当查询到达时,存储节点返回匹配的样本。这样,即使发布者已经离线,查询者仍能拿到最后已知状态。对于PX4参数、飞行模式、健康状态这类“慢变化但关键”的数据,存储机制非常实用。

查询还可以带selector和parameters,例如px4/vehicle/attitude?last=10表示取最近10条。存储后端可以配置为内存、SQLite、InfluxDB等,取决于是否需要持久化与历史分析。

# 使用zenoh-cli查询当前值
z_get 'px4/vehicle/attitude'

# 查询最近5条历史样本(需存储节点支持)
z_get 'px4/vehicle/attitude?last=5'

# 声明一个存储节点,保存px4/sensor/**的最新值
z_storage --key 'px4/sensor/**' --backend memory

实践建议:在PX4+ROS2桥接中,把高频控制流(如vehicle_attitude)只走发布订阅,避免存储带来额外延迟;把低频状态(如vehicle_status、参数、任务航点)同时接入存储,便于地面站随时查询与断线恢复。

五、案例与误区复盘

假设一个典型场景:无人机通过4G链路连接地面站,链路时断时续。若只用发布订阅,断线期间地面站完全丢失状态;若引入Zenoh存储节点部署在机载计算机上,地面站重连后可以先get一次px4/vehicle/**,立即恢复对飞行器状态的认知,再继续订阅实时流。这就是“查询+存储”与“发布订阅”协同的价值。

常见误区有三个:其一,把键表达式当成普通字符串,随意使用**导致订阅范围过大,浪费带宽;其二,认为存储节点必须中心化部署,实际上Zenoh存储可以分布式存在,甚至每个节点本地存一份;其三,混淆“查询”与“服务”,查询是数据检索,服务是远程调用,两者在Zenoh中虽然都走键空间,但语义不同。

排障提示:如果订阅者收不到数据,先检查键表达式是否匹配、发布者与订阅者是否在同一Zenoh域(或路由可达)、以及congestion_control是否因链路拥塞丢弃了样本。查询无响应时,优先确认是否有存储节点声明了对应键,以及查询参数是否被后端支持。

Zenoh 键空间上的三种交互:发布订阅 · 查询 · 存储 Zenoh 键空间(Key Space) px4/sensor/** · px4/vehicle/attitude · ros2/offboard/cmd_vel PX4 发布者 put(key, payload) uORB → Zenoh 桥接 ROS2 订阅者 declare_subscriber px4/sensor/** Zenoh 存储节点 订阅并保存最新值 memory / SQLite / InfluxDB 地面站查询者 get(key?last=N) 断线重连后恢复状态 发布 推送 订阅落盘 查询 存储节点响应查询,返回匹配样本 键表达式通配规则 * 匹配单层 · ** 匹配多层 · $* 参数化 发布 订阅 存储 查询

理解Zenoh的这四个关键词——设计哲学、发布订阅、键表达式、查询与存储——就掌握了它在PX4与ROS2之间扮演的角色:它不是简单的消息转发器,而是一个可寻址、可回溯、可裁剪的分布式数据空间。后续在配置桥接与调优时,所有参数与策略都会回到这四个概念上。