南向总览
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(数据转发队列) Gatewayforwarding 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 完全一致)
- 网关在创建 driver 时,会通过
关键语义
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 通用属性
| 字段 | 类型 | 说明 | 建议 |
|---|---|---|---|
name | string | 人类可读名称(日志/监控/诊断的首选标识) | 生产环境保持稳定命名,便于排障 |
driver_id | string | 绑定的 driver 工厂标识 | 与已安装 driver 一致 |
collection_type | Collection | Report | 采集类型:Collection 会被 Collector 轮询;Report 不参与轮询,主要依赖 driver 主动 Push(订阅/上报) | 协议/现场决定(Modbus/S7 常用 Collection) |
report_type | Always | Change | 上报策略:Always 全量上报;Change 由 core 维护 device 快照并做变化过滤(减少北向带宽与计算) | 高频点位建议 Change(并配合合理采集周期) |
period | number | 轮询周期(ms),仅在 collection_type == Collection 时生效 | 结合设备能力与吞吐预算设置 |
status | boolean | 启用/禁用。禁用 channel 不会被启动/轮询/路由 | 变更前先评估影响范围 |
connection_policy | object | 连接与超时/退避策略(字段由 core 提供,driver 在连接/读/写处使用) | 现场弱网建议启用退避并限制累计重试窗口 |
driver_config | object | driver 私有配置(形状由 driver 决定,用于连接/会话/协议层参数等) | 通过 driver 的 UI schema 配置;避免放敏感明文(如密码/密钥) |
connection_policy
| 字段 | 类型 | 说明 | 建议 |
|---|---|---|---|
connect_timeout_ms | number | 建立连接/会话握手超时(默认 10000ms) | 现场链路差可适当放大 |
read_timeout_ms | number | 协议读超时(默认 10000ms) | 与设备响应时间对齐 |
write_timeout_ms | number | 协议写超时(默认 10000ms) | 写入一般要更保守 |
backoff | RetryPolicy | 重连/重试的统一指数退避策略(driver 与北向插件复用同一模型) | 避免“重连风暴/惊群” |
connection_policy.backoff(RetryPolicy)
| 字段 | 类型 | 说明 | 建议 |
|---|---|---|---|
max_attempts | number | null | 最大重试次数;0 表示禁用重试;null 表示无限重试;设置为 N 表示最多重试 N 次 | 生产建议使用有限次数或有限时长 |
initial_interval_ms | number | 初始退避间隔(默认 1000ms) | 1000~3000 |
max_interval_ms | number | 最大退避上限(默认 30000ms) | 30000~60000 |
multiplier | number | 指数倍率(默认 2.0) | 2.0 |
randomization_factor | number | 抖动系数(默认 0.2,代表 ±20% jitter) | 0.1~0.3 |
max_elapsed_time_ms | number | null | 最大累计重试时长(默认 null,表示不限制;与 max_attempts 同时设置时先到者生效) | 建议设置,例如 10~30 分钟 |
Device 通用属性
| 字段 | 类型 | 说明 | 建议 |
|---|---|---|---|
device_name | string | 设备名称(用于 northward 编码与可观测性) | 与现场标识对齐 |
device_type | string | 设备类型/机型(用于 driver 做模型选择/差异化解析) | 作为 driver 的“分支 key”应稳定 |
channel_id | string | 所属 channel ID | 自动生成即可 |
status | boolean | 启用/禁用(禁用设备应在 driver 侧与 core 路由侧被跳过) | 灰度启用/停用便于排障 |
driver_config | object | null | device 级 driver 私有配置(可选) | 用于该设备的差异化参数;没需求就留空 |
Point 通用属性
| 字段 | 类型 | 说明 | 建议 |
|---|---|---|---|
id | string | point 唯一 ID(热路径主键,变化检测/快照索引优先用它) | 仅内部使用 |
device_id | string | 所属 device ID | 自动生成即可 |
name | string | 点位名称(人类可读) | 与现场图纸/变量名对齐 |
key | string | 点位稳定 key(对外引用/写回/主题路由的首选标识) | 必须稳定,避免改动破坏对接 |
type | Telemetry | Attribute | 点位类别 | 按用途正确建模 |
data_type | string | 值类型(bool/i32/f64/string/…) | 与协议真实值域一致 |
access_mode | Read | Write | ReadWrite | 访问模式 | 用它表达“安全边界” |
unit | string | 展示单位(如 ℃、kPa、A) | 尽量短;避免在热路径做字符串拼接 |
min_value / max_value | number | null | 写入范围约束(仅当 min 与 max 同时存在时生效) | 用于防误写;与值域保持一致 |
transform_data_type | string | null | 参数 logical data type。为空则 logical=wire | 影响下行输入校验类型 |
transform_scale | number | null | 比例系数 (s)。上行 wire→logical、下行 logical→wire 逆变换 | 下行要求 (s != 0) |
transform_offset | number | null | 偏移量 (o) | 与工程零点对齐 |
transform_negate | boolean | 是否取反(顺序同 Point) | 用于方向相反/符号翻转 |
driver_config | object | point 级 driver 私有配置(地址/寄存器/数据块/订阅项等协议细节) | 按 driver 文档配置;避免把协议细节写进 key/name |
Point 关键语义
access_mode的作用:- 采集侧:core 会按
access_mode过滤可读点位(Read/ReadWrite)用于采集;过滤可写点位(Write/ReadWrite)用于写入能力展示/路由。 - 写入侧:WritePoint 入口会用它做强校验;非
Write/ReadWrite会被直接拒绝并返回NotWriteable。
- 采集侧:core 会按
Read不是“协议不支持写”:它是产品/现场层面的安全边界;应在建模阶段正确配置,避免误写关键点位。ReadWrite一致性要求:driver 必须保证读写路径对同一地址/变量的语义一致(单位/比例/编码)。min_value/max_value的值域一致性:当前 core 的范围校验发生在 logical 值域(北向语义)。因此 min/max 必须与北向输入/输出处于同一值域(工程值)。- Transform 的一致性要求:一旦启用
transformScale/transformOffset/transformNegate或transformDataType,就必须同时保证:- 上行输出与下行写入使用同一套 logical 语义;
min/max按 logical 值域配置;- 避免在 driver 内重复应用 Transform(双重缩放会直接写错值)。
Action & Parameter 通用属性
Action
| 字段 | 类型 | 说明 | 建议 |
|---|---|---|---|
id | string | 动作唯一 ID | 仅内部使用 |
name | string | 动作名称(人类可读) | 面向运维/现场可读 |
device_id | string | 所属 device ID | 自动生成即可 |
command | string | driver 识别的命令名(协议/实现相关,但对外稳定) | 同设备内唯一且稳定 |
inputs | Parameter[] | 输入参数定义列表 | 用于 UI/校验/解析 |
Parameter
| 字段 | 类型 | 说明 | 建议 |
|---|---|---|---|
name / key | string | 参数展示名/稳定 key | key 必须稳定 |
data_type | string | 参数类型 | 与 driver 解析一致 |
required | boolean | 是否必填 | 必填参数尽量少 |
default_value | any | null | 默认值(如有) | 非必填建议提供默认值 |
min_value / max_value | number | null | 范围约束(如有) | 用于防误写 |
transform_data_type | string | null | 参数 logical data type。为空则 logical=wire | 影响下行输入校验类型 |
transform_scale | number | null | 比例系数 (s)。上行 wire→logical、下行 logical→wire 逆变换 | 下行要求 (s != 0) |
transform_offset | number | null | 偏移量 (o) | 与工程零点对齐 |
transform_negate | boolean | 是否取反(顺序同 Point) | 用于方向相反/符号翻转 |
driver_config | object | 参数级 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 放到后台任务。
