Skip to content

南向总览

NG Gateway 的南向体系不仅仅是“把设备接上来”,而是一个专为工业现场不确定性多协议并存高吞吐采集设计的接入层。

本文档将帮助你建立南向的正确心智模型,并理解数据如何被采集、归一化并进入核心管线与北向链路。

南向是什么

南向负责把“现实世界的设备/总线/控制器”稳定接入网关,并把读写结果标准化为统一的 NorthwardData 进入核心管线与北向。它的目标是可靠、可观测、可扩展、性能优先

设计原则

南向只解决“如何连接、如何采集、如何编解码、如何容错”;不要在 driver 里绑定北向协议、业务规则或平台差异。

心智模型

  • Driver:协议适配器(Modbus/S7/OPC UA/IEC104…),负责连接管理、协议编解码、读写语义与容错策略。

  • Channel(通道):driver 的一个“运行实例”。一个 channel 绑定一个 driver factory + 一份运行时配置(连接策略、采集策略等),其下面挂载多个device。
    Channel 是连接与会话的边界:同一协议的不同现场线路/PLC/服务器通常用不同 channel 隔离。

  • Device(设备):channel 下的一个采集对象(站点/PLC/表计/节点)。Device 的 device_type 用于 driver 做模型选择/差异化解析。

  • Point(点位):device 下的一个数据点(遥测/属性),携带点位类型、数据类型、访问模式(只读/只写/读写)以及单位/量程/比例等元信息。Point 的 key 是对外稳定标识,id 是网关内部高频热路径主键。

  • Action(动作):device 下的一个命令/RPC 定义(例如“合闸/分闸/复位/写参数”)。Action 的 command 是 driver 识别的命令名,输入参数由 Parameter 描述(类型/必填/默认值/范围)。

Polling vs Driver Push

当前网关有且仅有两条“南向 → 核心 → 北向”的数据路径;两条路径在进入 core 之后完全复用同一条转发/路由链路

1. Polling(轮询采集,由 Collector 调度)

  • 适用场景:Modbus/S7 等“读寄存器/读变量”为主的协议;或现场要求固定周期采集。
  • 触发机制:当且仅当 channel 的 collection_type == Collection 时,该 channel 才会被 Collector 拉起轮询任务;period 决定 tick 周期。
  • 核心链路
    • Collector 按 channel tick → 拉取 device/points → 按 driver.collection_group_key() 做物理分组(同 driver 实例内)
    • 每个 group:调用 driver.collect_data(items_in_group)(批量采集)
    • 返回 Vec<NorthwardData>(仍按业务 device 输出)→ 逐条发送到 core 的 bounded mpsc(数据转发队列)
    • Gateway forwarding task 从队列 recv → 广播给实时监控(realtime hub)→ NorthwardManager::route(快照/变化过滤 + 按“北向 app 订阅”路由到各 app)

关键语义

Polling 的“调度者”是 core 的 Collector,driver 只实现“如何高效读点位与解析”。

1.1 Grouped collection 的语义

强烈建议

把本小节当成“概览入口”,详细设计、并发/超时语义、以及各驱动实际用法请阅读:
Group Collection(分组采集)设计与各驱动用法

Grouped collection(也称 Group Collection)用于解决一个常见建模场景:同一条物理会话/连接下,为了业务组织把点位拆成多个业务 Device(例如同一 PLC / 同一 OPC UA endpoint、或同一 Modbus slave 被拆为多个业务设备)。

Collector 会调用 driver 提供的 collection_group_key(device) 来决定“哪些业务 Device 应该在同一批次 collect_data(items) 调用里合并采集”:

  • collection_group_key(device) -> None:不做物理分组。Collector 会保证 collect_data(items)items.len() == 1(单设备采集)。
  • collection_group_key(device) -> Some(key):做物理分组。Collector 会把同一 key 下的多个 device 合并成一次 collect_data(items) 调用。

其中 key 是一个固定大小的 CollectionGroupKey([u8;16]),包含:

  • kind(4 bytes 命名空间,用于跨驱动隔离)
  • payload(12 bytes 协议自定义负载,用于表达“物理会话语义”)

输入不变量(由 core 保证):

  • items 永不为空(空调用属于 bug)。
  • 如果 collection_group_key == None,则 items.len() == 1
  • 如果 collection_group_key == Some(key),则 items 中所有 device 都属于同一个 key
  • items 内部会按 device_id 升序构造,确保行为稳定、便于排障。

输出语义(driver 必须遵守):

  • 即使一次调用合并采集了多个 device,driver 仍然必须按业务 device 输出 NorthwardData(Telemetry/Attributes 的 device_id/device_name 不能混淆)。
  • 通常建议:同一 group 使用同一个 timestamp(保证本轮数据一致性)。

2. Driver Push(订阅/上报,由 Driver 自己驱动)

  • 适用场景:OPC UA subscription、IEC104 主动上送、DNP3 SOE、任何协议的异步事件/变化上报。
  • 触发机制:driver 在 start() 后自行建立订阅/监听/接收循环,遇到数据时直接 publish。
  • 核心链路(按实现)
    • 网关在创建 driver 时,会通过 SouthwardInitContext 注入一个 publisher: Arc<dyn NorthwardPublisher>
    • driver 在内部任务里调用 publisher.try_publish(Arc<NorthwardData>)(非阻塞,背压通过错误返回)
    • 数据进入同一条 core 转发队列 → forwarding task → NorthwardManager::route(与 Polling 完全一致)

关键语义

Subscription不是由 Collector 调度;它属于 driver 的“会话层/协议层”职责,core 只提供一个高性能的 publish 入口与后续统一路由。

反向路径

反向路径的共同目标是:在尽可能靠近入口处做“可判定”的校验与限流,避免把非法/高风险/洪峰请求直接打到现场设备。

点位写入

  • 入口:北向插件下行事件 NorthwardEvent::WritePoint
  • core 侧校验
    • NotFound:point_id 不存在。
    • NotWriteable:点位 access_mode 不是 Write/ReadWrite
    • TypeMismatch:写入值与点位 data_type 不匹配。
    • OutOfRange:仅数值类型;当 min_value 与 max_value 都存在 时,对写入值做区间校验。
    • NotConnected:所属 channel 未连接。
    • QueueTimeout:同一 channel 的写入串行队列等待超时。
  • 串行化与并发模型
    • 同一 channel 内写入严格串行(避免协议/设备不支持并发写导致的乱序与状态撕裂)。
    • 不同 channel 之间可并行(网关会把 WritePoint 处理丢进独立 task,充分利用多核与 I/O 并发)。
  • 执行:通过 driver.write_point(device, point, value, timeout_ms) 进入 driver。
  • 响应:写入完成后回传 NorthwardData::WritePointResponse(控制面响应不会被“数据背压”丢弃)。

动作/命令

  • 入口:北向插件下行事件 NorthwardEvent::CommandReceived
  • core 侧校验
    • NotFound:action 不存在。
    • TypeMismatch:写入值与点位 data_type 不匹配。
    • OutOfRange:仅数值类型;当 min_value 与 max_value 都存在 时,对写入值做区间校验。
    • NotConnected:所属 channel 未连接。
    • QueueTimeout:同一 channel 的写入串行队列等待超时。
  • 执行:通过 driver.execute_action(device, action, parameters) 进入 driver。
  • 响应:写入完成后回传 NorthwardData::RpcResponse(控制面响应不会被“数据背压”丢弃)。

通用属性

Channel 通用属性

字段类型说明建议
namestring人类可读名称(日志/监控/诊断的首选标识)生产环境保持稳定命名,便于排障
driver_idstring绑定的 driver 工厂标识与已安装 driver 一致
collection_typeCollection | Report采集类型:Collection 会被 Collector 轮询;Report 不参与轮询,主要依赖 driver 主动 Push(订阅/上报)协议/现场决定(Modbus/S7 常用 Collection
report_typeAlways | Change上报策略:Always 全量上报;Change 由 core 维护 device 快照并做变化过滤(减少北向带宽与计算)高频点位建议 Change(并配合合理采集周期)
periodnumber轮询周期(ms),仅在 collection_type == Collection 时生效结合设备能力与吞吐预算设置
statusboolean启用/禁用。禁用 channel 不会被启动/轮询/路由变更前先评估影响范围
connection_policyobject连接与超时/退避策略(字段由 core 提供,driver 在连接/读/写处使用)现场弱网建议启用退避并限制累计重试窗口
driver_configobjectdriver 私有配置(形状由 driver 决定,用于连接/会话/协议层参数等)通过 driver 的 UI schema 配置;避免放敏感明文(如密码/密钥)

connection_policy

字段类型说明建议
connect_timeout_msnumber建立连接/会话握手超时(默认 10000ms)现场链路差可适当放大
read_timeout_msnumber协议读超时(默认 10000ms)与设备响应时间对齐
write_timeout_msnumber协议写超时(默认 10000ms)写入一般要更保守
backoffRetryPolicy重连/重试的统一指数退避策略(driver 与北向插件复用同一模型)避免“重连风暴/惊群”

connection_policy.backoff(RetryPolicy)

字段类型说明建议
max_attemptsnumber | null最大重试次数;0 表示禁用重试;null 表示无限重试;设置为 N 表示最多重试 N生产建议使用有限次数或有限时长
initial_interval_msnumber初始退避间隔(默认 1000ms)1000~3000
max_interval_msnumber最大退避上限(默认 30000ms)30000~60000
multipliernumber指数倍率(默认 2.0)2.0
randomization_factornumber抖动系数(默认 0.2,代表 ±20% jitter)0.1~0.3
max_elapsed_time_msnumber | null最大累计重试时长(默认 null,表示不限制;与 max_attempts 同时设置时先到者生效)建议设置,例如 10~30 分钟

Device 通用属性

字段类型说明建议
device_namestring设备名称(用于 northward 编码与可观测性)与现场标识对齐
device_typestring设备类型/机型(用于 driver 做模型选择/差异化解析)作为 driver 的“分支 key”应稳定
channel_idstring所属 channel ID自动生成即可
statusboolean启用/禁用(禁用设备应在 driver 侧与 core 路由侧被跳过)灰度启用/停用便于排障
driver_configobject | nulldevice 级 driver 私有配置(可选)用于该设备的差异化参数;没需求就留空

Point 通用属性

字段类型说明建议
idstringpoint 唯一 ID(热路径主键,变化检测/快照索引优先用它)仅内部使用
device_idstring所属 device ID自动生成即可
namestring点位名称(人类可读)与现场图纸/变量名对齐
keystring点位稳定 key(对外引用/写回/主题路由的首选标识)必须稳定,避免改动破坏对接
typeTelemetry | Attribute点位类别按用途正确建模
data_typestring值类型(bool/i32/f64/string/…)与协议真实值域一致
access_modeRead | Write | ReadWrite访问模式用它表达“安全边界”
unitstring展示单位(如 ℃、kPa、A)尽量短;避免在热路径做字符串拼接
min_value / max_valuenumber | null写入范围约束(仅当 min 与 max 同时存在时生效)用于防误写;与值域保持一致
transform_data_typestring | null参数 logical data type。为空则 logical=wire影响下行输入校验类型
transform_scalenumber | null比例系数 (s)。上行 wire→logical、下行 logical→wire 逆变换下行要求 (s != 0)
transform_offsetnumber | null偏移量 (o)与工程零点对齐
transform_negateboolean是否取反(顺序同 Point)用于方向相反/符号翻转
driver_configobjectpoint 级 driver 私有配置(地址/寄存器/数据块/订阅项等协议细节)按 driver 文档配置;避免把协议细节写进 key/name

Point 关键语义

  • access_mode 的作用
    • 采集侧:core 会按 access_mode 过滤可读点位(Read/ReadWrite)用于采集;过滤可写点位(Write/ReadWrite)用于写入能力展示/路由。
    • 写入侧:WritePoint 入口会用它做强校验;非 Write/ReadWrite 会被直接拒绝并返回 NotWriteable
  • Read 不是“协议不支持写”:它是产品/现场层面的安全边界;应在建模阶段正确配置,避免误写关键点位。
  • ReadWrite 一致性要求:driver 必须保证读写路径对同一地址/变量的语义一致(单位/比例/编码)。
  • min_value/max_value 的值域一致性:当前 core 的范围校验发生在 logical 值域(北向语义)。因此 min/max 必须与北向输入/输出处于同一值域(工程值)。
  • Transform 的一致性要求:一旦启用 transformScale/transformOffset/transformNegatetransformDataType,就必须同时保证:
    • 上行输出与下行写入使用同一套 logical 语义;
    • min/max 按 logical 值域配置;
    • 避免在 driver 内重复应用 Transform(双重缩放会直接写错值)。

Action & Parameter 通用属性

Action

字段类型说明建议
idstring动作唯一 ID仅内部使用
namestring动作名称(人类可读)面向运维/现场可读
device_idstring所属 device ID自动生成即可
commandstringdriver 识别的命令名(协议/实现相关,但对外稳定)同设备内唯一且稳定
inputsParameter[]输入参数定义列表用于 UI/校验/解析

Parameter

字段类型说明建议
name / keystring参数展示名/稳定 keykey 必须稳定
data_typestring参数类型与 driver 解析一致
requiredboolean是否必填必填参数尽量少
default_valueany | null默认值(如有)非必填建议提供默认值
min_value / max_valuenumber | null范围约束(如有)用于防误写
transform_data_typestring | null参数 logical data type。为空则 logical=wire影响下行输入校验类型
transform_scalenumber | null比例系数 (s)。上行 wire→logical、下行 logical→wire 逆变换下行要求 (s != 0)
transform_offsetnumber | null偏移量 (o)与工程零点对齐
transform_negateboolean是否取反(顺序同 Point)用于方向相反/符号翻转
driver_configobject参数级 driver 私有配置(用于 driver 做协议层映射/编码/枚举等)通过 driver schema 配置;仅放 driver 必需信息

Parameter 关键语义(core 统一校验与解析)

  • 参数结构
    • 多参数动作:params 必须是 JSON Object(按 key 取值)。
    • 单参数动作:允许直接给标量(scalar),也允许给 {key: value}
  • 必填与默认值
    • required=true:必须提供(否则报错)。
    • required=false:允许不提供,但必须有 default_value(否则报错)。
  • 类型转换(尽量宽容,但可预测):会把 JSON scalar 尝试转换成目标 data_type(包含数字字符串、bool 字符串、时间戳、binary 的 base64/hex 等常见形式)。
  • 范围校验:当 Parameter 声明了 min_value/max_value 时,会对数值型输入做区间校验,并汇总成可读的错误信息返回。

最佳实践

背压与队列

  • publisher.try_publish 是非阻塞的:当 core 转发队列满时会返回 QueueFull(背压信号)。driver 必须决定策略:丢弃、聚合、降采样、重试(带退避),而不是在热路径里无界堆积。
  • 批量优先:Polling 场景下,driver 应尽量将一次采集结果组成少量 NorthwardData(例如按 device 分组),减少发送次数与调度开销。

轮询采集

  • 超时/重试/退避要可配置:使用 connection_policy 提供的超时与 backoff;连续失败要指数退避,避免重连风暴。
  • 批量读取策略:按协议能力将点位拆成批次(上限/对齐/地址连续性),并将批大小、并发度、超时做成可调参数。

订阅/上报(Subscription/Push)

  • 订阅循环要可取消:在 stop() 时保证能快速退出(配合取消令牌/会话生命周期)。
  • 噪声与抖动隔离:对频繁变化点位要有采样/节流,避免把北向/核心队列打满。

解析容错

  • 解析失败不 panic:坏帧/半包/CRC 错/乱序都必须返回可操作的错误语义,并携带足够上下文(channel/device/地址/计数器)。
  • 可恢复同步:出现坏帧应丢弃并重新同步帧头;出现短暂超时应可重试;出现认证失败/连接断开应触发重连路径。
  • 错误分级:区分“可重试/需重连/不可重试/需降级”,把“现场噪声”限制在本 channel 内,不扩散到全局。

运行时变更(RuntimeDelta)

网关支持在运行中对 device/point/action 做增删改并通知 driver(RuntimeDelta)。driver 的实现应:

  • 增量更新本地索引:避免全量重建;
  • 保证顺序与幂等:同一 channel 内 delta 需要按序处理;
  • 不要在持锁状态 await:更新结构应尽量快,I/O 放到后台任务。

基于 Apache License 2.0 许可发布.