# SCADA-PLATFORM **Repository Path**: watson0/scada-platform ## Basic Information - **Project Name**: SCADA-PLATFORM - **Description**: 一个插件式的跨平台scada程序 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-03-27 - **Last Updated**: 2026-05-15 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # SCADA 平台程序接口描述文档 V0.19.0 轻量级、插件化的工业数据采集与监控平台,目标运行环境为 ARM 嵌入式网关/工控机。 --- ## 目录 - [架构概述](#架构概述) - [插件类型](#插件类型) - [插件运行机制](#插件运行机制) - [启动方式](#启动方式) - [守护与重启](#守护与重启) - [数据库访问](#数据库访问) - [数据模型](#数据模型) - [采集通道](#采集通道) - [转发通道](#转发通道) - [插件 MQTT 接口规范](#插件-mqtt-接口规范) - [MQTT Broker 配置](#mqtt-broker-配置) - [采集插件(主站)MQTT 接口](#采集插件主站-mqtt-接口) - [转发插件(从站)MQTT 接口](#转发插件从站-mqtt-接口) - [官方 Go SDK](#官方-go-sdk) - [计算变量](#计算变量) - [人工置数](#人工置数) - [上位机集成接口](#上位机集成接口) - [配置快照](#配置快照) - [配置变更通知](#配置变更通知) - [向上位机注册](#向上位机注册) - [配置文件](#配置文件) - [串口映射](#串口映射serial_ports) - [非 Go 插件实现指引](#非-go-插件实现指引) - [目录结构](#目录结构) - [编译](#编译) - [部署](#部署) --- ## 架构概述 ``` ┌──────────────────────────────────────────────────────────┐ │ SCADA 平台 │ │ ┌──────────┐ ┌──────────┐ ┌────────┐ ┌───────────┐ │ │ │ Web 管理 │ │ 驱动管理器│ │MQTT订阅│ │ WebSocket │ │ │ │ (Vue 3) │ │(进程启停) │ │ 接收 │ │ 推送端 │ │ │ └──────────┘ └──────────┘ └───┬────┘ └─────┬─────┘ │ └──────────────────────────────────┼──────────────┼────────┘ │ MQTT Broker │ WS ┌──────────────┴──────────┐ Browser │ │ ┌──────────┴──────┐ ┌──────────┴──────┐ │ 采集插件(主站) │ │ 转发插件(从站) │ │ Modbus TCP │ │ Modbus TCP从站 │ │ IEC 60870-5-104│ │ IEC 104从站 │ │ OPC-UA │ │ OPC-UA服务端 │ └─────────────────┘ └─────────────────┘ 上行:采集 → MQTT 下行:MQTT → 响应外部请求 ``` **核心设计原则:平台完全不关心数据如何采集或转发,只负责数据的接收、存储、展示和指令下发。** 协议插件是独立的可执行程序,通过 MQTT 与平台通信。只要遵守本文档约定的接口,插件可以用任何语言实现任何协议。 --- ## 插件类型 平台支持两类插件,各自对应不同的通道类型: | 插件类型 | 对应通道 | 角色 | 数据流向 | |---------|---------|------|---------| | **采集插件(主站)** | 采集通道(Channel) | 主动连接外部设备,周期性轮询 | 采集 → 发布实时值到 MQTT | | **转发插件(从站)** | 转发通道(ForwardChannel) | 被动监听,响应外部主站请求 | 订阅 MQTT 实时值 → 响应外部读取请求 | --- ## 插件运行机制 ### 启动方式 平台启动时,分别查询采集通道和转发通道中 `is_open = 1` 的记录,按 `protocol` 字段找到对应可执行文件并启动: ``` protocols/ ├── modbus-tcp.exe ← 采集通道 channel.protocol = "modbus-tcp" ├── modbus-tcp-slave.exe ← 转发通道 forward_channel.protocol = "modbus-tcp-slave" ├── iec104.exe └── iec104-slave.exe ``` 平台传递唯一的启动参数: ```bash # 采集插件 ./protocols/modbus-tcp -channel-id=3 # 转发插件 ./protocols/modbus-tcp-slave -channel-id=2 ``` 两类插件均通过 `-channel-id` 接收自己的通道 ID,自行读取数据库获取完整配置。 ### 守护与重启 - 插件进程意外退出后,平台在 **5 秒**后自动重启(先确认通道仍然投运) - 主动关闭投运(`is_open` 改为 0)时,平台终止进程且不重启 - 删除通道时,平台同步停止对应插件进程并删除其日志文件 - 采集通道插件日志:`log/channel-{id}.log`(单文件 10 MB,保留 1 个压缩备份) - 转发通道插件日志:`log/forward-channel-{id}.log`(同上) ### 数据库访问 插件与平台共享同一个 SQLite 数据库(`db/data.db`),启动时通过 SDK 一次性加载所需配置,后续运行过程中不再读写数据库。 --- ## 数据模型 ### 采集通道 ``` 采集通道 (Channel) └── 设备 (Device) ← device_address: 从站地址/公共地址 └── 数据区 (DataArea) ← 对应一次批量读取请求 └── 变量 (Variable) ← 对应数据区内的一个具体测点 ``` | 层级 | 关键字段 | 说明 | |------|---------|------| | Channel | `protocol` | 插件可执行文件名(不含 .exe) | | Channel | `interval` | 采集周期(毫秒) | | Channel | `remote_address` / `remote_port` | TCP 连接目标 | | Channel | `serial_number` / `baud_rate` 等 | 串口参数 | | Device | `device_address` | 从站地址(Modbus Unit ID / 104 公共地址) | | DataArea | `modbus_function_code` | FC01/02/03/04 | | DataArea | `modbus_start_address` | 起始寄存器地址 | | DataArea | `modbus_register_number` | 读取寄存器数量 | | Variable | `addr` | 寄存器绝对地址 | | Variable | `data_type` | 数据类型(见下表) | | Variable | `collect_coefficient` | 采集系数,最终值 = 原始值 × 系数 | **data_type 枚举** | 值 | 类型 | |----|------| | 1 | Bits(位域) | | 2 | Uint8 | | 3 | Int8 | | 4 | Uint16 | | 5 | Int16 | | 6 | Uint32 | | 7 | Int32 | | 8 | Float32 | **code_type 枚举**(仅 32 位类型有效) | 值 | 字节序 | 说明 | |----|--------|------| | 1 | ABCD | 高字在前,高字节在前(标准大端) | | 2 | CDAB | 低字在前,高字节在前(最常见) | | 3 | BADC | 高字在前,低字节在前 | | 4 | DCBA | 低字在前,低字节在前(真小端) | ### 转发通道 ``` 转发通道 (ForwardChannel) └── 转发设备 (ForwardDevice) ← slave_id: 本机对外暴露的从站地址 └── 转发变量 (ForwardVariable) ← 将采集变量映射到寄存器地址 ``` | 层级 | 关键字段 | 说明 | |------|---------|------| | ForwardChannel | `protocol` | 从站插件可执行文件名 | | ForwardChannel | `local_address` / `local_port` | 本机监听地址和端口 | | ForwardDevice | `slave_id` | 对外暴露的从站地址(1-247) | | ForwardVariable | `register_type` | 1=线圈 2=离散输入 3=保持寄存器 4=输入寄存器 | | ForwardVariable | `register_address` | 寄存器地址(十进制,可切换十六进制显示) | | ForwardVariable | `variable_id` | 关联的采集变量 ID | | ForwardVariable | `name` | 转发变量名,默认沿用采集变量名 | --- ## 插件 MQTT 接口规范 > 这是平台插件化架构的核心契约。只要遵守此规范,插件可用任何语言实现任何工业协议。 > **平台只订阅,插件只发布**(转发从站插件例外,需额外订阅实时值)。 ### MQTT Broker 配置 插件从配置文件 `configs/config_dev.yaml` 的 `mqtt.broker` 字段读取 Broker 地址。 --- ### 采集插件(主站)MQTT 接口 采集插件需要发布以下三类消息,并在连接时设置 LWT。 #### 连接要求 | 参数 | 要求 | |------|------| | ClientID | 格式:`scada-driver-ch{channel_id}`,须全局唯一 | | KeepAlive | 建议 ≤ 10 秒 | | LWT Topic | `scada/status/{channel_id}` | | LWT Payload | 通道和所有设备均置 0(见状态上报格式) | | LWT QoS | 1 | 设置 LWT 是强制要求。插件异常崩溃时,Broker 自动代发 LWT,平台才能及时感知离线。 #### 1. 实时值上报 `scada/realtime/{channel_id}` 每个采集周期完成后发布一次,包含本周期内所有成功采集到的变量值。 **QoS:1** ```json { "ts": 1712345678901, "values": { "101": 3.14, "102": 0.0, "105": 1033.5 } } ``` | 字段 | 类型 | 说明 | |------|------|------| | `ts` | int64 | Unix 毫秒时间戳 | | `values` | object | key 为 **variable.id**(字符串),value 为 float64 | > 只上报本周期成功采集到的变量。采集失败的变量不上报,平台侧保留上一次的值。 #### 2. 通讯报文上报 `scada/frames/{channel_id}` 每次收发一帧原始报文时发布,用于前端通讯细节展示与调试。 **QoS:0**(尽力而为,不影响可靠性) ```json { "device_id": 5, "direction": "tx", "hex": "00 01 00 00 00 06 01 03 00 00 00 0A", "ts": 1712345678901 } ``` | 字段 | 类型 | 说明 | |------|------|------| | `device_id` | int | 当前正在通讯的 device.id | | `direction` | string | `"tx"` 发送 / `"rx"` 接收 | | `hex` | string | 原始报文十六进制,字节间空格分隔,大写 | | `ts` | int64 | Unix 毫秒时间戳 | > 此接口协议无关,Modbus、104、OPC-UA 等均使用相同格式。 #### 3. 状态上报 `scada/status/{channel_id}` 每个采集周期完成后发布,反映通道和设备的在线状态。 **QoS:1** ```json { "ts": 1712345678901, "channel_status": 1, "device_status": { "5": 1, "6": 1, "7": 0 } } ``` | 字段 | 类型 | 说明 | |------|------|------| | `ts` | int64 | Unix 毫秒时间戳 | | `channel_status` | int | `1` 在线 / `0` 离线 | | `device_status` | object | key 为 **device.id**(字符串),value 为 `1`/`0` | **判断标准(由插件自行决定):** - 通道在线:与目标设备/服务器的连接正常 - 设备在线:本周期内该设备至少有一个数据区读取成功 - 设备离线:本周期内该设备所有数据区读取均失败 **超时机制:** 平台侧超过 **10 秒**未收到状态更新,自动将该通道及所有设备标记为离线并推送前端。 --- #### 4. 订阅:遥控指令 `scada/control/{channel_id}` 平台下发遥控命令时向此 Topic 发布消息,插件订阅后执行写操作。 **QoS:0** 支持两种类型,由 `type` 字段区分。 **变量遥控(`type: "variable"`)** ```json { "cmd_id": "550e8400-e29b-41d4-a716-446655440000", "ts": 1712345678901, "type": "variable", "control_mode": "execute", "device_id": 5, "variable_id": 101, "value": 100.0 } ``` `control_mode` 取值: | 值 | 含义 | 适用协议 | |----|------|---------| | `execute`(或缺省)| 直接执行写操作 | Modbus / IEC104 直控 | | `select` | 预置选择(SBO 第一步) | IEC104 选择-执行 | | `cancel` | 撤销预置 | IEC104 | > Modbus 插件只响应 `execute`,收到 `select` / `cancel` 时忽略并不发回执。 插件收到后根据 `variable_id` 查本地配置,推导写地址和功能码,自动选择正确的写操作: | 数据区读功能码 | 写操作 | |--------------|--------| | FC01 / FC02(线圈区) | FC05 写单个线圈,`value != 0` 为 ON | | FC03 / FC04(寄存器区),单寄存器(Uint16/Int16) | FC06 写单个寄存器 | | FC03 / FC04(寄存器区),多寄存器(Uint32/Int32/Float32) | FC16 写多个寄存器,按变量 `code_type` 编码 | **协议原生遥控(`type: "modbus_raw"`)** 适合调试场景,由调用方直接指定功能码和寄存器。 ```json { "cmd_id": "550e8400-e29b-41d4-a716-446655440000", "ts": 1712345678901, "type": "modbus_raw", "slave_id": 1, "function_code": 6, "address": 100, "value": 1234 } ``` FC15/FC16 多值时,`value` 换成 `values` 数组(uint16 原始值,连续地址): ```json { "cmd_id": "...", "ts": 1712345678901, "type": "modbus_raw", "slave_id": 1, "function_code": 16, "address": 100, "values": [1234, 5678] } ``` | 字段 | 类型 | 说明 | |------|------|------| | `cmd_id` | string | 指令唯一 ID(UUID),用于回执关联 | | `ts` | int64 | Unix 毫秒时间戳 | | `type` | string | `"variable"` 或 `"modbus_raw"` | | `control_mode` | string | `execute`(缺省)/ `select` / `cancel` | | `device_id` | int | (variable 类型)device.id | | `variable_id` | int | (variable 类型)variable.id | | `value` | float64 | 单值(FC05/FC06 或 variable 类型) | | `slave_id` | int | (modbus_raw 类型)从站地址 | | `function_code` | int | (modbus_raw 类型)5 / 6 / 15 / 16 | | `address` | int | (modbus_raw 类型)起始寄存器/线圈地址 | | `values` | array of float64 | (modbus_raw FC15/FC16)多个连续寄存器值 | > 插件收到不认识的 `type` 时直接丢弃,不发回执。`modbus_raw` 类型不支持 `control_mode`,始终视为直接执行。 --- #### 5. 发布:遥控回执 `scada/control/result/{channel_id}` 插件执行完遥控指令后发布回执,平台据此返回 HTTP 响应给前端。 **QoS:0** ```json { "cmd_id": "550e8400-e29b-41d4-a716-446655440000", "success": true, "error": "" } ``` | 字段 | 类型 | 说明 | |------|------|------| | `cmd_id` | string | 与指令中的 `cmd_id` 一致,用于关联 | | `success` | bool | 执行成功为 `true` | | `error` | string | 失败时的错误描述,成功时为空字符串 | > 平台等待回执超时为 **5 秒**,超时视为失败并返回"遥控超时"错误。插件应在写操作完成(或报错)后立即发布回执,不应等待下一个采集周期。 --- ### 转发插件(从站)MQTT 接口 转发插件是被动服务端,订阅实时值缓存,响应外部主站的读取请求,并上报通讯报文。 #### 连接要求 | 参数 | 要求 | |------|------| | ClientID | 格式:`scada-slave-ch{forward_channel_id}`,须全局唯一 | | 无需 LWT | 从站无设备状态概念,连接断开不影响平台状态 | #### 订阅:实时值 `scada/realtime/#` 订阅所有采集通道的实时值,维护本地内存缓存(variable_id → float64)。Payload 格式与采集插件上报格式相同。 ```json { "ts": 1712345678901, "values": { "101": 3.14, "102": 0.0 } } ``` #### 发布:通讯报文 `scada/frames/{forward_channel_id}` 与采集插件使用**同一个 Topic**,通过 `forward: true` 字段区分。前端通讯细节页面统一展示。 **QoS:0** ```json { "device_id": 3, "direction": "rx", "hex": "00 01 00 00 00 06 01 03 00 00 00 0A", "ts": 1712345678901, "forward": true } ``` | 字段 | 类型 | 说明 | |------|------|------| | `device_id` | int | 当前通讯的 **forward_device.id** | | `direction` | string | `"rx"` 收到请求 / `"tx"` 发出响应 | | `hex` | string | 原始报文十六进制,字节间空格分隔,大写 | | `ts` | int64 | Unix 毫秒时间戳 | | `forward` | bool | **必须为 `true`**,平台据此区分采集报文和转发报文 | > `device_id` 与采集通道的 device.id 是不同表的 ID,通过 `forward: true` 避免混淆。 --- ## 官方 Go SDK 如果插件使用 Go 实现,可直接使用官方 SDK,无需手动处理 MQTT 连接和 Payload 序列化。 ### 采集插件(主站) ```go // 1. 加载采集通道完整配置(通道→设备→数据区→变量) cfg, err := sdk.LoadChannelConfig(db, channelID) // 2. 创建发布者(自动设置 LWT,插件崩溃时 Broker 代发离线状态) deviceIDs := []int{5, 6, 7} // 该通道下所有设备 ID pub, err := sdk.NewPublisher(mqttBroker, channelID, deviceIDs) defer pub.Close() // 3. 发布实时值 pub.PublishValues(map[int]float64{101: 3.14, 102: 0.0}) // 4. 发布通讯报文 pub.PublishFrame(deviceID, true, rawBytes) // tx:发出的帧 pub.PublishFrame(deviceID, false, rawBytes) // rx:收到的帧 // 5. 发布通道/设备在线状态 pub.PublishStatus(true, map[int]bool{5: true, 6: true, 7: false}) // 6. 订阅遥控指令(在采集主循环外,回调将指令放入队列,主循环消费) pub.SubscribeControl(func(cmd sdk.ControlCmd) { cmdChan <- cmd }) // 7. 发布遥控回执 pub.PublishControlResult(sdk.ControlResult{ CmdID: cmd.CmdID, Success: true, Error: "", }) ``` ### 转发插件(从站) ```go // 1. 加载转发通道完整配置(转发通道→转发设备→转发变量) cfg, err := sdk.LoadForwardChannelConfig(db, channelID) // 2. 自行创建 MQTT 客户端(无需 LWT) opts := pahomqtt.NewClientOptions().AddBroker(broker).SetClientID(...) client := pahomqtt.NewClient(opts) client.Connect() // 3. 订阅实时值,维护本地缓存 client.Subscribe("scada/realtime/#", 0, func(msg) { /* 更新缓存 */ }) // 4. 发布通讯报文(与采集插件同一 topic,payload 带 forward=true) sdk.PublishForwardFrame(client, forwardChannelID, forwardDeviceID, false, rawBytes) // rx sdk.PublishForwardFrame(client, forwardChannelID, forwardDeviceID, true, rawBytes) // tx ``` Modbus 报文解析工具包在 `protocol/modbusutils`,提供 FC01/02/03/04 读取和多字节序解码。 --- ## 计算变量 计算变量是平台内置的虚拟变量类型,不从硬件采集,而是由平台内部的计算引擎按公式周期性求值,结果写入实时值存储并推送给前端和转发插件。 ### 两种类型 | 类型 | `is_calculated` | 归属 | 典型用途 | |------|----------------|------|---------| | 设备计算变量 | `1` | 挂在某个采集设备下 | 同设备多个寄存器合并运算,如功率 = 电压 × 电流 | | 系统计算变量 | `2` | 不属于任何设备(`device_id = null`) | 跨通道跨设备的全局汇总,如全厂总用电量 | ### 公式语法 公式用 `{变量ID}` 引用任意采集变量,支持标准算术运算符和括号: ``` {1001} + {1002} * 0.001 ({71} + {72} + {73} + {74}) / 4 {101} * {102} / 1000.0 ``` 底层使用 [expr-lang/expr](https://github.com/expr-lang/expr) 库,公式在引擎启动时一次性编译为字节码。 **依赖变量就绪策略:** 若公式引用的任意一个变量在实时值存储中尚无数据,当前 tick 跳过,不产生输出,直到所有依赖均有值后才开始计算。 ### 数据库字段(`variables` 表) | 字段 | 类型 | 说明 | |------|------|------| | `is_calculated` | int | `0`/NULL = 采集变量,`1` = 设备计算变量,`2` = 系统计算变量 | | `formula` | string | 计算公式字符串,`{varId}` 占位符 | | `calc_interval` | int | 计算周期(毫秒),默认 1000,最小 100 | | `data_area_id` | int | **固定为 `-1`**(哨兵值),满足 not null 约束但不属于任何真实数据区 | | `addr` | int | **固定为 `-1`**,同上 | | `device_id` | int/null | 设备计算变量填实际设备 ID;系统计算变量为 `null` | ### HTTP API | 方法 | 路径 | 说明 | |------|------|------| | GET | `/v1/calc-variable/` | 列出所有计算变量(含设备/通道路径) | | POST | `/v1/calc-variable/` | 新建计算变量 | | POST | `/v1/calc-variable/validate` | 仅校验公式语法,不写库 | | GET | `/v1/calc-variable/:id` | 获取单条,含 `param_labels`(公式中每个 `{ID}` 对应的变量路径) | | PUT | `/v1/calc-variable/:id` | 更新,自动热重载计算引擎 | | DELETE | `/v1/calc-variable/:id` | 删除,自动停止对应计算任务 | ### 计算引擎 引擎(`internal/calcengine`)在平台启动时初始化,每个计算变量独立一个 goroutine: ``` 启动 → 从 DB 加载所有 is_calculated IN (1,2) → 编译公式 → 启动 ticker goroutine 每 tick → 读 rtstore 拿依赖值 → expr.Run 求值 → 写 rtstore + WS 广播 + MQTT 发布 ``` CRUD 操作后通过 `global.GLOBAL_CALC_ENGINE.Reload(id)` / `Remove(id)` 实现热重载,无需重启平台。 ### MQTT 行为 计算引擎产出的实时值发布到**虚拟通道 0**,转发插件通过通配符订阅自动收到: | Topic | QoS | retain | 说明 | |-------|-----|--------|------| | `scada/realtime/0` | 1 | false | 每 tick 增量推送 | | `scada/snapshot/0` | 1 | **true** | retain 保留最新值,转发插件重启后可直接获取 | Payload 格式与普通采集变量完全相同:`{"ts": ms, "values": {"varId": value}}`。 --- ## 人工置数 人工置数是一种运行时覆盖机制:操作员可以为某个变量设定一个固定值,该值将替代来自设备的实时采集值,直到手动解除。常用于传感器故障维修期间、调试联调或临时补偿等场景。 ### 工作原理 置数状态存在两层: 1. **持久层(数据库)**:变量表记录 `is_forced = 1` 和当前置数值 `value`,平台重启后可自动恢复。 2. **运行层(内存)**:`rtstore` 内维护一个置数集合,所有写入 rtstore 的入口(MQTT 订阅、计算引擎)在写入前均检查该集合,已置数的变量会被跳过,不允许覆盖。 数据优先级:**人工置数值 > 设备采集值 / 计算结果**。 ### HTTP API | 方法 | 路径 | 说明 | |------|------|------| | POST | `/v1/variable/:id/force` | 置数,body: `{"value": 3.14}` | | DELETE | `/v1/variable/:id/force` | 解除置数,立即恢复采集 | ### WebSocket 广播格式 置数后平台立即向所有 WS 客户端广播,消息结构与普通实时值更新相同,但多出 `forced: true` 字段: ```json { "type": "update", "data": { "101": { "v": 3.14, "ts": 1712345678901, "forced": true } } } ``` --- ## 上位机集成接口 上位机系统需要感知 SCADA 平台的配置结构(通道→设备→变量)以及配置变化。推荐的集成模式为: ``` 上位机启动 ├─ SCADA向上位机注册(SCADA 主动 POST,上位机被动接收 worker_id + port) └─ 调用 GET /v1/config/snapshot → 拿完整配置树,缓存在内存 上位机运行期间 └─ 订阅 MQTT scada/config/changed(批量消息,含防抖聚合) ├─ 遍历 changes 数组 │ ├─ op=create/update → 按 ids 重新拉 snapshot 或局部更新缓存 │ └─ op=delete → 按 type + ids 批量从缓存中移除,无需 HTTP └─ 若长时间未收到通知,主动重拉一次 snapshot 对齐状态 ``` ### 配置快照 #### `GET /v1/config/snapshot` 一次请求返回完整的通道→设备→数据区→变量树,无需多次分页查询。 **无需鉴权,直接调用。** **响应示例** ```json { "code": 200, "msg": "ok", "data": [ { "id": 1, "name": "Modbus采集", "protocol": "modbus-tcp", "protocol_type": "modbus", "is_open": 1, "remote_address": "192.168.1.100", "remote_port": 502, "devices": [ { "id": 10, "name": "PLC-01", "device_address": 1, "is_open": 1, "channel_id": 1, "data_areas": [ { "id": 100, "name": "保持寄存器", "area_type": 1, "modbus_function_code": 3, "modbus_start_address": 0, "modbus_register_number": 10, "device_id": 10, "variables": [ { "id": 1001, "name": "温度", "data_type": 5, "addr": 0, "collect_coefficient": 10, "data_area_id": 100, "device_id": 10 } ] } ] } ] } ] } ``` **说明** | 字段 | 说明 | |------|------| | `data` | 通道数组,每个通道嵌套设备,设备嵌套数据区,数据区嵌套变量 | | 各层 `id` | 全局唯一,上位机用此 ID 关联实时值和配置变更通知 | | `variables[].id` | 即实时值中的 `variable_id`,上位机用此 ID 从 rtstore 读取实时值 | **调用示例(curl)** ```bash curl http://localhost:8080/v1/config/snapshot ``` **调用示例(Go)** ```go resp, err := http.Get("http://localhost:8080/v1/config/snapshot") body, _ := io.ReadAll(resp.Body) // 解析 body 中的 data 字段即为完整配置树 ``` --- ### 配置变更通知 #### MQTT Topic:`scada/config/changed` 平台在任何通道、设备、数据区、变量发生增删改时,自动向此 Topic 发布通知。上位机订阅后可实时感知配置变化,无需轮询。 **防抖聚合机制:** 平台内置 3000 ms 静默窗口,短时间内的多条变更(例如批量删除 1000 个变量)会被合并成一条批量消息发出,避免消息风暴。 **QoS:1**(至少送达一次) **Payload 格式** ```json { "changes": [ { "type": "variable", "op": "delete", "ids": [1001, 1002, 1003] }, { "type": "device", "op": "delete", "ids": [10] } ] } ``` | 字段 | 类型 | 取值 | 说明 | |------|------|------|------| | `changes` | array | — | 变更分组列表,同一消息可包含多个类型/操作的组合 | | `changes[].type` | string | `channel` / `device` / `data_area` / `variable` / `forward_channel` / `forward_device` / `forward_variable` | 变更的资源类型 | | `changes[].op` | string | `create` / `update` / `delete` | 操作类型 | | `changes[].ids` | array of int | 对应资源的数据库 ID 列表 | `delete` 时这些 ID 已从数据库删除 | **上位机处理建议** ``` 收到消息后,遍历 changes 数组: 对于每个 ChangeGroup: delete → 按 type + ids 批量从缓存中移除,无需 HTTP 请求 create / update → 重新拉取 snapshot,或按 ids 逐条拉取局部更新 ``` | `op` | 建议操作 | |------|---------| | `create` | 重新拉取 snapshot,或按 `ids` 单独请求新增资源插入缓存 | | `update` | 重新拉取 snapshot,或按 `ids` 单独请求对应资源更新缓存 | | `delete` | 直接从缓存中按 `type` + `ids` 批量删除,无需发 HTTP 请求 | > **注意:** 若 MQTT 未连接,平台会在日志(`log/scada.log`)中打印 WARN 级别提示,此次通知会被跳过,上位机下次连接 MQTT 后需主动重拉一次 snapshot 对齐状态。 --- ### 向上位机注册 平台启动后会主动向上位机发送一次注册请求,告知自己的身份和监听端口,便于上位机动态发现并管理多台 SCADA 采集机。 #### 配置 ```yaml registration: enabled: true # false 则不发请求 backend: http://192.168.1.10:8080/api/scada/register # 上位机接收注册的 HTTP 地址 ``` #### worker_id 文件 `worker_id` 是本机的唯一身份标识,在设备出厂时烧录,存放于 SCADA 主程序同级目录下的 `worker_id` 文件(纯文本,一行内容): ``` /opt/scada/ ├── scada ← SCADA 主程序 └── worker_id ← 内容例:GW-20240001 ``` 开发环境(`go run`)时,平台同时在当前工作目录查找该文件作为兜底。 #### 请求格式 ``` POST {backend} Content-Type: application/json ``` ```json { "worker_id": "GW-20240001", "port": 8080 } ``` | 字段 | 类型 | 说明 | |------|------|------| | `worker_id` | string | 从 `worker_id` 文件读取的设备 ID | | `port` | int | SCADA 平台 HTTP 服务监听端口 | 上位机通过 HTTP 请求的来源 IP + 此处的 `port` 即可完整定位该 SCADA 实例的接口地址,无需 payload 中携带 IP。 #### 重试策略 - 请求失败时最多重试 **3 次**,每次间隔 **10 秒** - 全部失败后记录 ERROR 日志并放弃,不影响平台正常运行 - 没有心跳,注册只在启动时发送一次 #### 上位机接口要求 上位机的接收端点需返回 `2xx` 状态码表示注册成功,其他状态码视为失败并触发重试。 --- ## 配置文件 配置文件位于 `cmd/configs/config_dev.yaml`(开发)或 `cmd/configs/config.yaml`(生产)。以下说明平台特有的配置段。 ### 串口映射(serial_ports) 记录串口编号到 Linux 设备路径的映射,前端通道配置页从此处读取可用串口列表,以下拉框形式展示给用户。 ```yaml serial_ports: 1: /dev/ttyS1 2: /dev/ttyS2 3: /dev/ttyS3 4: /dev/ttyS4 ``` - 键为整数型串口编号,值为 Linux 设备文件路径 - 若此段为空或未配置,前端串口号字段显示提示"未配置串口映射,请在 config.yaml 的 serial_ports 中添加" - 实际串口设备因硬件不同而异(如 `/dev/ttyUSB0`、`/dev/ttyAMA0`),需根据目标硬件配置 #### HTTP API | 方法 | 路径 | 说明 | |------|------|------| | GET | `/v1/serial-ports` | 返回当前配置的串口列表,按编号升序排列 | **响应示例** ```json { "code": 200, "msg": "ok", "data": [ { "number": 1, "path": "/dev/ttyS1" }, { "number": 2, "path": "/dev/ttyS2" } ] } ``` ### 向上位机注册(registration) 见[向上位机注册](#向上位机注册)章节。 --- ## 非 Go 插件实现指引 任何语言均可实现插件,分别满足以下要求: ### 采集插件(主站) 1. **接收 CLI 参数** `-channel-id=N` 2. **读取数据库**(SQLite)获取通道、设备、数据区、变量配置 3. **连接 MQTT Broker**,设置 LWT(`scada/status/{id}` 全部离线 payload) 4. **周期性采集**,发布实时值、通讯报文、状态三类消息 5. **处理退出信号**(SIGTERM/SIGINT) ### 转发插件(从站) 1. **接收 CLI 参数** `-channel-id=N`(这里的 ID 是转发通道 ID) 2. **读取数据库**获取转发通道、转发设备、转发变量配置,构建 unitID→寄存器→变量ID 的映射表 3. **连接 MQTT Broker**,订阅 `scada/realtime/#`,维护实时值缓存 4. **监听本地 TCP 端口**(`local_address:local_port`),接受多连接 5. **解析外部主站请求**,从缓存查询对应变量值,构造响应 6. **发布通讯报文**到 `scada/frames/{forward_channel_id}`,带 `forward: true` 7. **处理退出信号** --- ## 目录结构 ``` cmd/ # 平台主程序入口 configs/ # 配置文件 protocols/ # 协议插件可执行文件存放目录(平台从此处启动插件) log/ # 插件日志 channel-{id}.log # 采集通道插件日志(10 MB 自动滚动,保留 1 个压缩备份) forward-channel-{id}.log# 转发通道插件日志(同上,删除通道时自动清理) internal/ # 平台核心代码 api/v1/ # HTTP 路由注册 controller/ # HTTP 处理器 service/ # 业务逻辑 repository/ # 数据库操作 model/ # 数据模型 driver/ manager.go # 采集通道插件进程管理器 forward_manager.go # 转发通道插件进程管理器 mqtt/ # MQTT 订阅者(统一处理采集和转发报文) ws/ # WebSocket Hub rtstore/ # 实时值内存存储 statusstore/ # 在线状态内存存储 confignotify/ # 配置变更防抖聚合通知器(200 ms 静默窗口) registration/ # 平台启动时向上位机注册(读取 worker_id 文件) protocol/ # 官方插件 sdk/ db.go # 数据库配置加载(LoadChannelConfig / LoadForwardChannelConfig) publisher.go # MQTT 发布接口(Publisher / PublishForwardFrame) modbusutils/ # Modbus 报文解析工具 modbus-tcp/ # Modbus TCP 主站采集插件 modbus-tcp-slave/ # Modbus TCP 从站转发插件 web/ # 前端(Vue 3 + TypeScript) ``` --- ## 编译 ### 主程序 ```shell # 在 cmd/ 目录下运行 go run . ``` ### 前端 ```shell # 在 web/ 目录下运行 npm run build ``` ### 协议插件 ```shell # 在 cmd/ 目录下运行,编译结果输出到 protocols/ go build ..\protocol\modbus-tcp ``` --- ## 部署 ```shell # 第一步:提交代码 git commit -m "feat: 版本信息" # 第二步:打标签 git tag v0.1.0 git push origin --tags ``` ### 版本号规范 ``` v 主版本 . 次版本 . 补丁 v 1 . 2 . 3 ↑ ↑ ↑ 破坏性改动 新功能 bug修复 ```