配置参数修改为从设备上同步后,再修改。
This commit is contained in:
parent
226b788afc
commit
978d5c5378
328
.trae/documents/2026-05-25-modbus-backend-refactor-plan.md
Normal file
328
.trae/documents/2026-05-25-modbus-backend-refactor-plan.md
Normal file
@ -0,0 +1,328 @@
|
|||||||
|
# Modbus 新协议后端改造说明
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
本次改造目标是让后端完全按 [显示界面modbus协议寄存器定义.md](file:///d:/Trae_space/EMCP/document/%E6%98%BE%E7%A4%BA%E7%95%8C%E9%9D%A2modbus%E5%8D%8F%E8%AE%AE%E5%AF%84%E5%AD%98%E5%99%A8%E5%AE%9A%E4%B9%89.md) 的新版协议工作,并取消 `backend/config` 作为配置真值来源的角色。
|
||||||
|
|
||||||
|
本次计划确认的边界如下:
|
||||||
|
|
||||||
|
- 只调整后端代码,不修改前端代码。
|
||||||
|
- 配置页面首次加载时,后端需先从下位机通过 Modbus 读取参数并返回。
|
||||||
|
- 界面保存配置时,后端只动态写入下位机,不再把配置写入 `backend/config` 下的 `device.json`、`channel.json`、`setting.json`。
|
||||||
|
- 协议文档中没有定义寄存器的现有配置接口,本次先保留,并继续沿用原本地配置逻辑作为过渡。
|
||||||
|
- 先生成本地说明文档,用户确认后再开始改代码。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current State Analysis
|
||||||
|
|
||||||
|
### 1. 当前接口层现状
|
||||||
|
|
||||||
|
当前配置接口集中在 `backend/app/api/routes/config.py`:
|
||||||
|
|
||||||
|
- 保留协议可能覆盖的接口:
|
||||||
|
- `GET/POST /api/config/channel`
|
||||||
|
- `GET/POST /api/config/line_alarm_setting`
|
||||||
|
- `GET/POST /api/config/ai_alarm_setting`
|
||||||
|
- `GET/POST /api/config/system`
|
||||||
|
- 当前存在但新版协议未覆盖的接口:
|
||||||
|
- `GET/POST /api/config/device`
|
||||||
|
- `POST /api/config/device/password`
|
||||||
|
- `GET/POST /api/config/device/net`
|
||||||
|
- `GET/POST /api/config/device/uart`
|
||||||
|
|
||||||
|
根据用户本轮决策,上述“协议未定义寄存器”的配置接口在本次后端改造先保留,并继续沿用原有本地配置文件逻辑。
|
||||||
|
|
||||||
|
### 2. 当前业务层现状
|
||||||
|
|
||||||
|
当前业务主要集中在 `backend/app/services/platform_service.py`,存在两个关键问题:
|
||||||
|
|
||||||
|
- 配置读取默认来自本地 JSON:
|
||||||
|
- `get_device_config()`
|
||||||
|
- `get_channel_config()`
|
||||||
|
- `get_line_alarm_setting()`
|
||||||
|
- `get_ai_alarm_setting()`
|
||||||
|
- `get_system_config()`
|
||||||
|
- 配置保存采用“先写本地 JSON,再下发设备”的双写模式:
|
||||||
|
- `save_device_config()`
|
||||||
|
- `save_channel_config()`
|
||||||
|
- `save_line_alarm_setting()`
|
||||||
|
- `save_ai_alarm_setting()`
|
||||||
|
- `save_system_config()`
|
||||||
|
|
||||||
|
这与“首次从下位机读取显示、保存时只写下位机、不再落本地配置文件”的新需求直接冲突。
|
||||||
|
|
||||||
|
此外,实时数据一次值计算当前仍依赖 `setting.json -> line_alarm_setting[].transformer_change[]` 中的 PT/CT 变比;如果配置文件取消真值角色,则这部分变比来源也必须切到设备回读数据。
|
||||||
|
|
||||||
|
### 3. 当前设备适配层现状
|
||||||
|
|
||||||
|
真实 Modbus 设备适配集中在 `backend/app/adapters/device_client.py`,当前实现与新版协议的差异点如下:
|
||||||
|
|
||||||
|
- 已符合或基本可复用:
|
||||||
|
- 四路实时数据输入寄存器读取
|
||||||
|
- AI 输入 `0x00D0 ~ 0x00DE`
|
||||||
|
- B 码对时 `0x00E0 ~ 0x00E2`
|
||||||
|
- 开入读取 `0x00E3`
|
||||||
|
- 开出线圈写 `0x0F`
|
||||||
|
- 系统时间同步 `0x0000`
|
||||||
|
- 明显不符合新版协议:
|
||||||
|
- 当前事件总寄存器仍按旧逻辑读取 `0x00E4`,新版应为 `0x00E5`
|
||||||
|
- 当前“读事件”按旧逻辑读保持寄存器 `0x0000 ~ 0x0003`,新版应改为读输入寄存器 `0x0100`,长度 `5 * n`
|
||||||
|
- 当前 AI/AO/AI 报警写入地址仍是旧版:
|
||||||
|
- AI 通道:`0x004C`,新版应为 `0x0054`
|
||||||
|
- AO 通道:`0x006A`,新版应为 `0x0072`
|
||||||
|
- AI 报警:`0x0088`,新版应为 `0x0090`
|
||||||
|
- 当前线路定值写入长度按 14 寄存器/每路处理,未包含 2 个变比参数;新版总长度为 64,并明确包含变比参数
|
||||||
|
- 当前没有“参数区回读”能力,只有写寄存器,没有从设备读取参数区并还原为 API 返回结构
|
||||||
|
|
||||||
|
### 4. 当前配置仓储现状
|
||||||
|
|
||||||
|
`backend/app/repositories/json_config_repo.py` 目前负责:
|
||||||
|
|
||||||
|
- 写 `device.json`
|
||||||
|
- 写 `channel.json`
|
||||||
|
- 写 `setting.json`
|
||||||
|
- 读上述 JSON 作为配置真值
|
||||||
|
|
||||||
|
按照本次需求,`backend/config` 中的配置文件不再承担运行期配置真值角色;对应仓储将不再被配置接口和业务服务依赖,只能保留给极少数非配置场景,或在后续整理中弱化/下线。
|
||||||
|
|
||||||
|
### 5. 当前测试现状
|
||||||
|
|
||||||
|
- `backend/tests/test_api.py` 大量依赖本地 JSON 写入与读取断言
|
||||||
|
- `backend/tests/test_modbus_client.py` 使用 FakeTransport 验证旧版寄存器地址、旧版事件读取方式和旧版参数写入地址
|
||||||
|
|
||||||
|
这些测试在本次改造后会大面积失效,必须同步调整。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Proposed Changes
|
||||||
|
|
||||||
|
### 1. 调整配置接口边界
|
||||||
|
|
||||||
|
#### 文件
|
||||||
|
|
||||||
|
- `backend/app/api/routes/config.py`
|
||||||
|
|
||||||
|
#### 改动内容
|
||||||
|
|
||||||
|
- 保留并继续支持的接口分为两类:
|
||||||
|
- **按新版协议改造为纯 Modbus 真值**:
|
||||||
|
- `GET/POST /api/config/channel`
|
||||||
|
- `GET/POST /api/config/line_alarm_setting`
|
||||||
|
- `GET/POST /api/config/ai_alarm_setting`
|
||||||
|
- `GET/POST /api/config/system`
|
||||||
|
- **协议未定义寄存器、先保留原本地逻辑**:
|
||||||
|
- `GET/POST /api/config/device`
|
||||||
|
- `POST /api/config/device/password`
|
||||||
|
- `GET/POST /api/config/device/net`
|
||||||
|
- `GET/POST /api/config/device/uart`
|
||||||
|
|
||||||
|
#### 原因
|
||||||
|
|
||||||
|
- 新协议文档已覆盖的配置项,需切换为“设备寄存器即真值”
|
||||||
|
- 新协议文档未覆盖的配置项,用户明确要求先保留接口,避免立即影响现有前端调用
|
||||||
|
- 这些未定义项本轮继续沿用本地配置文件逻辑,待后续协议补齐后再迁移
|
||||||
|
|
||||||
|
### 2. 配置读取改为“首次从下位机读”
|
||||||
|
|
||||||
|
#### 文件
|
||||||
|
|
||||||
|
- `backend/app/services/platform_service.py`
|
||||||
|
- `backend/app/adapters/device_client.py`
|
||||||
|
|
||||||
|
#### 改动内容
|
||||||
|
|
||||||
|
- 移除 `get_channel_config()` 对 `channel.json` 的依赖,改为调用设备侧“参数区回读”能力
|
||||||
|
- 移除 `get_line_alarm_setting()` 对 `setting.json` 的依赖,改为读取线路定值寄存器并按 `line_no` 解析
|
||||||
|
- 移除 `get_ai_alarm_setting()` 对 `setting.json` 的依赖,改为读取 AI 报警寄存器并还原 API 结构
|
||||||
|
- 移除 `get_system_config()` 对 `setting.json` 的依赖,改为从设备读取可回读的系统参数
|
||||||
|
- PT/CT 变比不再从 `setting.json` 读取,而改为从线路定值寄存器回读结果中提取
|
||||||
|
|
||||||
|
#### 原因
|
||||||
|
|
||||||
|
- 配置页面首次加载必须从下位机读取显示
|
||||||
|
- `backend/config` 不再保存配置真值
|
||||||
|
|
||||||
|
### 3. 配置保存改为“只写下位机,不写本地 JSON”
|
||||||
|
|
||||||
|
#### 文件
|
||||||
|
|
||||||
|
- `backend/app/services/platform_service.py`
|
||||||
|
- `backend/app/repositories/json_config_repo.py`
|
||||||
|
|
||||||
|
#### 改动内容
|
||||||
|
|
||||||
|
- 删除以下方法中的本地 JSON 写入逻辑:
|
||||||
|
- `save_channel_config()`
|
||||||
|
- `save_line_alarm_setting()`
|
||||||
|
- `save_ai_alarm_setting()`
|
||||||
|
- `save_system_config()`
|
||||||
|
- 返回值改为仅描述 Modbus 写入结果,不再返回本地 `save_path`
|
||||||
|
- 业务层不再把配置结果回写 `channel.json` / `setting.json`
|
||||||
|
- `JsonConfigRepository` 不再作为上述“协议已定义项”的运行期依赖
|
||||||
|
- `device/device password/device net/device uart` 相关接口暂不纳入此调整,仍继续使用本地配置文件逻辑
|
||||||
|
|
||||||
|
#### 原因
|
||||||
|
|
||||||
|
- 用户已明确要求取消本地配置文件落盘
|
||||||
|
- 配置应以设备当前寄存器值为唯一真值
|
||||||
|
|
||||||
|
### 4. 按新版协议更新寄存器地址和事件机制
|
||||||
|
|
||||||
|
#### 文件
|
||||||
|
|
||||||
|
- `backend/app/adapters/device_client.py`
|
||||||
|
|
||||||
|
#### 改动内容
|
||||||
|
|
||||||
|
- 更新常量地址:
|
||||||
|
- 事件总寄存器:由旧值改为 `0x00E5`
|
||||||
|
- 读事件起始地址:改为 `0x0100`
|
||||||
|
- AI 通道写入起始地址:改为 `0x0054`
|
||||||
|
- AO 通道写入起始地址:改为 `0x0072`
|
||||||
|
- AI 报警写入起始地址:改为 `0x0090`
|
||||||
|
- 新增/修改事件读取实现:
|
||||||
|
- 先读 `0x00E5`
|
||||||
|
- 若值为 `n > 0`,按 `0x04 + 0x0100 + 5*n` 读取输入寄存器
|
||||||
|
- 解析事件结构:时间戳、毫秒、线路号、事件类型、事件代码、动作值
|
||||||
|
- 读完后通过 `0x03 + 0x0100 + n` 发送“读事件成功回复”
|
||||||
|
- 新增/修改事件映射逻辑:
|
||||||
|
- 保护类
|
||||||
|
- 动作类
|
||||||
|
- 报警类
|
||||||
|
- 操作类
|
||||||
|
- AI 越限类
|
||||||
|
|
||||||
|
#### 原因
|
||||||
|
|
||||||
|
- 当前事件实现仍停留在旧协议,和最新文档不一致
|
||||||
|
|
||||||
|
### 5. 补齐参数区回读能力
|
||||||
|
|
||||||
|
#### 文件
|
||||||
|
|
||||||
|
- `backend/app/adapters/device_client.py`
|
||||||
|
- `backend/app/services/platform_service.py`
|
||||||
|
- `backend/app/schemas/platform.py`
|
||||||
|
|
||||||
|
#### 改动内容
|
||||||
|
|
||||||
|
- 在 `device_client.py` 中新增参数回读方法,例如:
|
||||||
|
- 读取串口设置寄存器块
|
||||||
|
- 读取线路定值设置寄存器块
|
||||||
|
- 读取 AI 通道设置寄存器块
|
||||||
|
- 读取 AO 通道设置寄存器块
|
||||||
|
- 读取 AI 报警设置寄存器块
|
||||||
|
- 读取系统设置寄存器块(仅限协议已定义项)
|
||||||
|
- 将回读寄存器块解析成后端当前接口使用的结构:
|
||||||
|
- `ChannelConfigIn`
|
||||||
|
- `LineAlarmSettingIn`
|
||||||
|
- `AiAlarmSettingIn`
|
||||||
|
- `SystemConfigIn` 的可支持子集
|
||||||
|
- 根据新版协议更新线路定值寄存器长度与解析方式:
|
||||||
|
- 从原先 7 类规则扩展到包含 2 个变比参数
|
||||||
|
- PT/CT 变比参与一次值换算
|
||||||
|
|
||||||
|
#### 原因
|
||||||
|
|
||||||
|
- 没有回读能力,就无法满足“参数页首次加载先从下位机读取显示”
|
||||||
|
|
||||||
|
### 6. 处理不再受协议支持的字段
|
||||||
|
|
||||||
|
#### 文件
|
||||||
|
|
||||||
|
- `backend/app/schemas/platform.py`
|
||||||
|
- `backend/app/services/platform_service.py`
|
||||||
|
- `backend/app/api/routes/config.py`
|
||||||
|
|
||||||
|
#### 改动内容
|
||||||
|
|
||||||
|
- 识别协议未覆盖字段并做“分层处理”
|
||||||
|
- 对 `SystemConfigIn` 中协议未定义的字段(如 `brightness`、`screen_saver`)进行重新评估:
|
||||||
|
- 若协议无寄存器,则从“纯 Modbus 真值范围”中剥离
|
||||||
|
- `GET/POST /config/system` 需按协议实际可支持字段精简或拆分
|
||||||
|
- 对历史仅本地保存的密码、网卡、版本号等结构:
|
||||||
|
- 本轮不从后端 API 中移除
|
||||||
|
- 继续保留 schema 与接口
|
||||||
|
- 继续沿用本地配置文件逻辑作为兼容过渡
|
||||||
|
- 在计划与代码实现中明确区分两类配置:
|
||||||
|
- 协议已定义项:首次回读设备、保存只写设备、不落本地
|
||||||
|
- 协议未定义项:接口先保留,继续本地逻辑
|
||||||
|
|
||||||
|
#### 原因
|
||||||
|
|
||||||
|
- 用户已确认:协议未定义寄存器的配置接口先保留,不直接移除,并继续沿用原本地配置逻辑
|
||||||
|
|
||||||
|
### 7. 重写测试,验证“纯设备真值”模式
|
||||||
|
|
||||||
|
#### 文件
|
||||||
|
|
||||||
|
- `backend/tests/test_modbus_client.py`
|
||||||
|
- `backend/tests/test_api.py`
|
||||||
|
|
||||||
|
#### 改动内容
|
||||||
|
|
||||||
|
- 更新 `test_modbus_client.py`:
|
||||||
|
- 使用新版地址与长度断言
|
||||||
|
- 验证事件总寄存器 `0x00E5`
|
||||||
|
- 验证“读事件内容 `0x0100` + 成功回复 `0x0100`”完整链路
|
||||||
|
- 验证 AI/AO/AI报警新地址
|
||||||
|
- 验证线路定值包含变比参数
|
||||||
|
- 更新 `test_api.py`:
|
||||||
|
- 删除“协议已定义项”对本地 JSON 写入/读取的依赖型断言
|
||||||
|
- 改成 Mock/Stub 设备回读数据断言
|
||||||
|
- 增加“首次 GET 来自设备回读、POST 只触发设备写入”的测试
|
||||||
|
- 保留“协议未定义接口”的现有本地逻辑测试,或按兼容过渡方式调整断言
|
||||||
|
|
||||||
|
#### 原因
|
||||||
|
|
||||||
|
- 当前测试体系大量基于本地 JSON 真值,必须同步迁移
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Assumptions & Decisions
|
||||||
|
|
||||||
|
### 已确认决策
|
||||||
|
|
||||||
|
- 只修改后端,前端本次不动
|
||||||
|
- 按新版协议文档改后端 Modbus 行为
|
||||||
|
- `backend/config` 不再保存配置参数真值
|
||||||
|
- 参数页面首次加载时,后端先从下位机读取显示
|
||||||
|
- 配置保存时,后端只动态写入下位机
|
||||||
|
- 协议未定义寄存器的现有配置接口,本次先保留
|
||||||
|
- 协议未定义寄存器的现有配置接口,本次继续沿用原本地配置逻辑
|
||||||
|
|
||||||
|
### 当前计划中的实现假设
|
||||||
|
|
||||||
|
1. `line_alarm_setting` 中的 2 个变比参数可映射到现有 `transformer_change` 结构,或需在 schema 中进一步显式化为 PT/CT 变比字段。
|
||||||
|
2. `system` 配置若协议只定义时间同步,则该接口最终可能会缩减为仅时间设置相关能力。
|
||||||
|
3. 实时数据一次值计算仍由后端负责,但 PT/CT 变比来源将变更为设备回读参数,而非本地 JSON。
|
||||||
|
4. `json_config_repo.py` 会退出“协议已定义项”的配置主链路,但仍保留给协议未定义接口使用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification Steps
|
||||||
|
|
||||||
|
1. 代码层验证
|
||||||
|
- 协议已定义项的配置 GET 接口都不再访问 `backend/config/*.json`
|
||||||
|
- 协议已定义项的配置 POST 接口都不再写本地 JSON
|
||||||
|
- `device_client.py` 的寄存器常量与新版文档一致
|
||||||
|
|
||||||
|
2. 自动化测试验证
|
||||||
|
- `backend/tests/test_modbus_client.py` 通过
|
||||||
|
- `backend/tests/test_api.py` 通过
|
||||||
|
- 删除或更新“协议已定义项”依赖本地配置文件真值的旧断言
|
||||||
|
- 保留或改造“协议未定义项”的本地逻辑断言
|
||||||
|
|
||||||
|
3. 联调验证
|
||||||
|
- 手工调用协议已定义项的配置 GET 接口时,能从下位机回读数据
|
||||||
|
- 手工调用协议已定义项的配置 POST 接口时,只触发 Modbus 写,不产生本地配置文件变更
|
||||||
|
- 手工调用协议未定义项接口时,仍保持当前本地逻辑可用
|
||||||
|
- 事件轮询流程符合:
|
||||||
|
- 轮询 `0x00E5`
|
||||||
|
- 读 `0x0100`
|
||||||
|
- 成功回复 `0x0100`
|
||||||
|
|
||||||
|
4. 回归验证
|
||||||
|
- 实时数据、设备状态、报警历史、开出控制功能不因本次改造而退化
|
||||||
|
- WebSocket 轮询链路仍能持续推送数据
|
||||||
@ -18,6 +18,7 @@ from app.schemas.platform import (
|
|||||||
RealtimeData,
|
RealtimeData,
|
||||||
SystemConfigIn,
|
SystemConfigIn,
|
||||||
SwitchControlIn,
|
SwitchControlIn,
|
||||||
|
TimeSyncConfigIn,
|
||||||
ValueGroup,
|
ValueGroup,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -35,6 +36,26 @@ class DeviceClient(ABC):
|
|||||||
def read_alarm_events(self) -> List[AlarmEvent]:
|
def read_alarm_events(self) -> List[AlarmEvent]:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def read_channel_config(self) -> Dict[str, Any]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def read_line_alarm_setting(self, line_no: int) -> Dict[str, Any]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def read_ai_alarm_setting(self) -> List[Dict[str, Any]]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def read_system_config(self) -> Dict[str, Any]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def read_time_sync_config(self) -> Dict[str, str]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def send_device_config(self, payload: DeviceConfigIn) -> Dict[str, Any]:
|
def send_device_config(self, payload: DeviceConfigIn) -> Dict[str, Any]:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
@ -55,6 +76,10 @@ class DeviceClient(ABC):
|
|||||||
def send_system_config(self, payload: SystemConfigIn) -> Dict[str, Any]:
|
def send_system_config(self, payload: SystemConfigIn) -> Dict[str, Any]:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def send_time_sync_config(self, payload: TimeSyncConfigIn) -> Dict[str, Any]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def send_switch_control(self, payload: SwitchControlIn) -> Dict[str, Any]:
|
def send_switch_control(self, payload: SwitchControlIn) -> Dict[str, Any]:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
@ -93,15 +118,17 @@ LINE_REGISTER_COUNT = 0x34
|
|||||||
AI_INPUT_START_REGISTER = 0x00D0
|
AI_INPUT_START_REGISTER = 0x00D0
|
||||||
AI_INPUT_COUNT = 8
|
AI_INPUT_COUNT = 8
|
||||||
DI_INPUT_REGISTER = 0x00E3
|
DI_INPUT_REGISTER = 0x00E3
|
||||||
EVENT_FLAG_REGISTER = 0x00E4
|
DO_INPUT_REGISTER = 0x00E4
|
||||||
EVENT_HOLDING_START_REGISTER = 0x0000
|
EVENT_FLAG_REGISTER = 0x00E5
|
||||||
|
EVENT_INPUT_START_REGISTER = 0x0100
|
||||||
|
EVENT_SUCCESS_REPLY_REGISTER = 0x0100
|
||||||
UART_CONFIG_START_REGISTER = 0x0004
|
UART_CONFIG_START_REGISTER = 0x0004
|
||||||
UART_CONFIG_REGISTER_COUNT = 16
|
UART_CONFIG_REGISTER_COUNT = 16
|
||||||
LINE_ALARM_START_REGISTER = 0x0014
|
LINE_ALARM_START_REGISTER = 0x0014
|
||||||
LINE_ALARM_REGISTERS_PER_LINE = 14
|
LINE_ALARM_REGISTERS_PER_LINE = 16
|
||||||
AI_CHANNEL_START_REGISTER = 0x004C
|
AI_CHANNEL_START_REGISTER = 0x0054
|
||||||
AO_CHANNEL_START_REGISTER = 0x006A
|
AO_CHANNEL_START_REGISTER = 0x0072
|
||||||
AI_ALARM_START_REGISTER = 0x0088
|
AI_ALARM_START_REGISTER = 0x0090
|
||||||
ANALOG_BLOCK_REGISTER_COUNT = 30
|
ANALOG_BLOCK_REGISTER_COUNT = 30
|
||||||
DO_STATE_REGISTER = 0x00A6
|
DO_STATE_REGISTER = 0x00A6
|
||||||
TIME_SYNC_START_REGISTER = 0x0000
|
TIME_SYNC_START_REGISTER = 0x0000
|
||||||
@ -262,6 +289,46 @@ class MockDeviceClient(DeviceClient):
|
|||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._random = Random(3568)
|
self._random = Random(3568)
|
||||||
self._tick = 0
|
self._tick = 0
|
||||||
|
self._channel_config = {
|
||||||
|
"ai_channel": [
|
||||||
|
{"ch": 1, "singal_type": "4-20mA", "line_no": 1, "type": "UA", "limit_low": 0, "limit_high": 20}
|
||||||
|
],
|
||||||
|
"ao_channel": [
|
||||||
|
{"ch": 1, "singal_type": "1~5V", "line_no": 2, "type": "Q", "limit_low": 0, "limit_high": 20}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
self._line_alarm_settings = {
|
||||||
|
1: {
|
||||||
|
"line_no": 1,
|
||||||
|
"over_limit_alarm": [
|
||||||
|
{"category": "电压", "limit": 180, "delay": 180, "output_node": "开出1", "enabled": True},
|
||||||
|
{"category": "电流", "limit": 180, "delay": 180, "output_node": "开出1", "enabled": True},
|
||||||
|
{"category": "差流", "limit": 180, "delay": 180, "output_node": "开出1", "enabled": False},
|
||||||
|
{"category": "功率", "limit": 180, "delay": 180, "output_node": "开出1", "enabled": False},
|
||||||
|
{"category": "频率", "limit": 50, "delay": 180, "output_node": "开出1", "enabled": False},
|
||||||
|
],
|
||||||
|
"fault_alarm": [
|
||||||
|
{"category": "PT断线", "limit": 0, "delay": 180, "output_node": "开出1", "enabled": True},
|
||||||
|
{"category": "CT断线", "limit": 0, "delay": 180, "output_node": "开出2", "enabled": False},
|
||||||
|
],
|
||||||
|
"transformer_change": [
|
||||||
|
{"category": "PT变化", "value": 100.0, "enabled": True},
|
||||||
|
{"category": "CT变化", "value": 50.0, "enabled": True},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self._ai_alarm_setting = [
|
||||||
|
{
|
||||||
|
"channel_no": 1,
|
||||||
|
"singal_type": "4-20mA",
|
||||||
|
"limit_low": 0,
|
||||||
|
"limit_high": 20,
|
||||||
|
"delay": 180,
|
||||||
|
"output_node": "开出1",
|
||||||
|
"enabled": True,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
self._system_config = {"time_sync": "2026-05-25 12:34:56", "brightness": 80, "screen_saver": 60}
|
||||||
|
|
||||||
def _value_group(self, base_u: float, base_i: float, base_p: float) -> ValueGroup:
|
def _value_group(self, base_u: float, base_i: float, base_p: float) -> ValueGroup:
|
||||||
delta = self._tick % 5
|
delta = self._tick % 5
|
||||||
@ -330,10 +397,42 @@ class MockDeviceClient(DeviceClient):
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def read_channel_config(self) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"ai_channel": [dict(item) for item in self._channel_config["ai_channel"]],
|
||||||
|
"ao_channel": [dict(item) for item in self._channel_config["ao_channel"]],
|
||||||
|
}
|
||||||
|
|
||||||
|
def read_line_alarm_setting(self, line_no: int) -> Dict[str, Any]:
|
||||||
|
if line_no in self._line_alarm_settings:
|
||||||
|
current = self._line_alarm_settings[line_no]
|
||||||
|
return {
|
||||||
|
"line_no": current["line_no"],
|
||||||
|
"over_limit_alarm": [dict(item) for item in current["over_limit_alarm"]],
|
||||||
|
"fault_alarm": [dict(item) for item in current["fault_alarm"]],
|
||||||
|
"transformer_change": [dict(item) for item in current["transformer_change"]],
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"line_no": line_no,
|
||||||
|
"over_limit_alarm": [],
|
||||||
|
"fault_alarm": [],
|
||||||
|
"transformer_change": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
def read_ai_alarm_setting(self) -> List[Dict[str, Any]]:
|
||||||
|
return [dict(item) for item in self._ai_alarm_setting]
|
||||||
|
|
||||||
|
def read_system_config(self) -> Dict[str, Any]:
|
||||||
|
return dict(self._system_config)
|
||||||
|
|
||||||
|
def read_time_sync_config(self) -> Dict[str, str]:
|
||||||
|
return {"time_sync": str(self._system_config.get("time_sync", ""))}
|
||||||
|
|
||||||
def send_device_config(self, payload: DeviceConfigIn) -> Dict[str, Any]:
|
def send_device_config(self, payload: DeviceConfigIn) -> Dict[str, Any]:
|
||||||
return {"send_status": "成功", "target": "device", "items": len(payload.net) + len(payload.uart)}
|
return {"send_status": "成功", "target": "device", "items": len(payload.net) + len(payload.uart)}
|
||||||
|
|
||||||
def send_channel_config(self, payload: ChannelConfigIn) -> Dict[str, Any]:
|
def send_channel_config(self, payload: ChannelConfigIn) -> Dict[str, Any]:
|
||||||
|
self._channel_config = payload.model_dump()
|
||||||
return {
|
return {
|
||||||
"send_status": "成功",
|
"send_status": "成功",
|
||||||
"target": "channel",
|
"target": "channel",
|
||||||
@ -341,14 +440,21 @@ class MockDeviceClient(DeviceClient):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def send_line_alarm_setting(self, payload: LineAlarmSettingIn) -> Dict[str, Any]:
|
def send_line_alarm_setting(self, payload: LineAlarmSettingIn) -> Dict[str, Any]:
|
||||||
|
self._line_alarm_settings[payload.line_no] = payload.model_dump()
|
||||||
return {"send_status": "成功", "target": "line_alarm", "line_no": payload.line_no}
|
return {"send_status": "成功", "target": "line_alarm", "line_no": payload.line_no}
|
||||||
|
|
||||||
def send_ai_alarm_setting(self, payload: List[Dict[str, Any]]) -> Dict[str, Any]:
|
def send_ai_alarm_setting(self, payload: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||||
|
self._ai_alarm_setting = [dict(item) for item in payload]
|
||||||
return {"send_status": "成功", "target": "ai_alarm", "items": len(payload)}
|
return {"send_status": "成功", "target": "ai_alarm", "items": len(payload)}
|
||||||
|
|
||||||
def send_system_config(self, payload: SystemConfigIn) -> Dict[str, Any]:
|
def send_system_config(self, payload: SystemConfigIn) -> Dict[str, Any]:
|
||||||
|
self._system_config.update(payload.model_dump())
|
||||||
return {"send_status": "成功", "target": "system", "brightness": payload.brightness}
|
return {"send_status": "成功", "target": "system", "brightness": payload.brightness}
|
||||||
|
|
||||||
|
def send_time_sync_config(self, payload: TimeSyncConfigIn) -> Dict[str, Any]:
|
||||||
|
self._system_config["time_sync"] = payload.time_sync
|
||||||
|
return {"send_status": "成功", "target": "time_sync", "time_sync": payload.time_sync}
|
||||||
|
|
||||||
def send_switch_control(self, payload: SwitchControlIn) -> Dict[str, Any]:
|
def send_switch_control(self, payload: SwitchControlIn) -> Dict[str, Any]:
|
||||||
action_text = "合" if payload.action == 1 else "分"
|
action_text = "合" if payload.action == 1 else "分"
|
||||||
return {"control_status": f"执行成功: 开关{payload.ch}{action_text}"}
|
return {"control_status": f"执行成功: 开关{payload.ch}{action_text}"}
|
||||||
@ -392,6 +498,12 @@ class CDeviceClient(DeviceClient):
|
|||||||
payload += b"\x00"
|
payload += b"\x00"
|
||||||
return [(payload[index] << 8) | payload[index + 1] for index in range(0, len(payload), 2)]
|
return [(payload[index] << 8) | payload[index + 1] for index in range(0, len(payload), 2)]
|
||||||
|
|
||||||
|
def _registers_to_bytes(self, registers: Sequence[int]) -> bytes:
|
||||||
|
raw = bytearray()
|
||||||
|
for register in registers:
|
||||||
|
raw.extend([(int(register) >> 8) & 0xFF, int(register) & 0xFF])
|
||||||
|
return bytes(raw)
|
||||||
|
|
||||||
def _build_value_group(self, registers: Sequence[int]) -> ValueGroup:
|
def _build_value_group(self, registers: Sequence[int]) -> ValueGroup:
|
||||||
values: Dict[str, float] = {}
|
values: Dict[str, float] = {}
|
||||||
for index, field_name in enumerate(VALUE_GROUP_FIELDS):
|
for index, field_name in enumerate(VALUE_GROUP_FIELDS):
|
||||||
@ -424,12 +536,17 @@ class CDeviceClient(DeviceClient):
|
|||||||
|
|
||||||
def _read_do_state(self) -> Dict[str, int]:
|
def _read_do_state(self) -> Dict[str, int]:
|
||||||
try:
|
try:
|
||||||
bits = self.transport.read_coils(0x0000, 12)
|
register = self.transport.read_input_registers(DO_INPUT_REGISTER, 1)[0]
|
||||||
if bits:
|
for index in range(1, 13):
|
||||||
for index, bit in enumerate(bits[:12], start=1):
|
self._do_state[index] = int(bool(register & (1 << (index - 1))))
|
||||||
self._do_state[index] = int(bool(bit))
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
try:
|
||||||
|
bits = self.transport.read_coils(0x0000, 12)
|
||||||
|
if bits:
|
||||||
|
for index, bit in enumerate(bits[:12], start=1):
|
||||||
|
self._do_state[index] = int(bool(bit))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return {f"do{index}": self._do_state.get(index, 0) for index in range(1, 13)}
|
return {f"do{index}": self._do_state.get(index, 0) for index in range(1, 13)}
|
||||||
|
|
||||||
def _build_status(self) -> DeviceStatus:
|
def _build_status(self) -> DeviceStatus:
|
||||||
@ -500,6 +617,134 @@ class CDeviceClient(DeviceClient):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _channel_payload_bytes(self, items: List[Any]) -> bytes:
|
||||||
|
raw = bytearray()
|
||||||
|
channel_map = {int(item.ch): item for item in items}
|
||||||
|
for channel_no in range(1, 13):
|
||||||
|
item = channel_map.get(channel_no)
|
||||||
|
if item is None:
|
||||||
|
raw.extend(b"\x00" * 5)
|
||||||
|
continue
|
||||||
|
raw.extend(
|
||||||
|
bytes(
|
||||||
|
[
|
||||||
|
self._signal_type_code(item.singal_type),
|
||||||
|
_clamp_byte(item.line_no),
|
||||||
|
self._measure_type_code(item.type),
|
||||||
|
_clamp_byte(item.limit_high),
|
||||||
|
_clamp_byte(item.limit_low),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return bytes(raw)
|
||||||
|
|
||||||
|
def _decode_signal_type(self, code: int) -> str:
|
||||||
|
return "4-20mA" if int(code) == 0 else "1~5V"
|
||||||
|
|
||||||
|
def _decode_measure_type(self, code: int) -> str:
|
||||||
|
return {
|
||||||
|
0: "UA",
|
||||||
|
1: "UB",
|
||||||
|
2: "UC",
|
||||||
|
3: "UAB",
|
||||||
|
4: "UBC",
|
||||||
|
5: "UCA",
|
||||||
|
6: "P",
|
||||||
|
7: "Q",
|
||||||
|
8: "F",
|
||||||
|
}.get(int(code), "UA")
|
||||||
|
|
||||||
|
def _decode_output_node(self, code: int) -> str:
|
||||||
|
return "" if int(code) <= 0 else f"开出{int(code)}"
|
||||||
|
|
||||||
|
def _decode_channel_items(self, registers: Sequence[int]) -> List[Dict[str, Any]]:
|
||||||
|
payload = self._registers_to_bytes(registers)
|
||||||
|
items: List[Dict[str, Any]] = []
|
||||||
|
for channel_no in range(1, 13):
|
||||||
|
start = (channel_no - 1) * 5
|
||||||
|
chunk = payload[start : start + 5]
|
||||||
|
if len(chunk) < 5:
|
||||||
|
chunk = chunk + b"\x00" * (5 - len(chunk))
|
||||||
|
items.append(
|
||||||
|
{
|
||||||
|
"ch": channel_no,
|
||||||
|
"singal_type": self._decode_signal_type(chunk[0]),
|
||||||
|
"line_no": int(chunk[1]),
|
||||||
|
"type": self._decode_measure_type(chunk[2]),
|
||||||
|
"limit_low": float(chunk[4]),
|
||||||
|
"limit_high": float(chunk[3]),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return items
|
||||||
|
|
||||||
|
def _decode_ai_alarm_items(self, registers: Sequence[int]) -> List[Dict[str, Any]]:
|
||||||
|
payload = self._registers_to_bytes(registers)
|
||||||
|
items: List[Dict[str, Any]] = []
|
||||||
|
for channel_no in range(1, 13):
|
||||||
|
start = (channel_no - 1) * 5
|
||||||
|
chunk = payload[start : start + 5]
|
||||||
|
if len(chunk) < 5:
|
||||||
|
chunk = chunk + b"\x00" * (5 - len(chunk))
|
||||||
|
items.append(
|
||||||
|
{
|
||||||
|
"channel_no": channel_no,
|
||||||
|
"singal_type": "4-20mA",
|
||||||
|
"limit_low": float(chunk[1]),
|
||||||
|
"limit_high": float(chunk[0]),
|
||||||
|
"delay": int(chunk[2]),
|
||||||
|
"output_node": self._decode_output_node(chunk[3]),
|
||||||
|
"enabled": bool(chunk[4]),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return items
|
||||||
|
|
||||||
|
def _encode_transformer_value(self, value: Any) -> int:
|
||||||
|
try:
|
||||||
|
number = int(round(float(value)))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
number = 1
|
||||||
|
return max(1, min(0xFFFF, number))
|
||||||
|
|
||||||
|
def _decode_transformer_items(self, payload: bytes) -> List[Dict[str, Any]]:
|
||||||
|
pt_ratio = int.from_bytes(payload[28:30], "big", signed=False) if len(payload) >= 30 else 1
|
||||||
|
ct_ratio = int.from_bytes(payload[30:32], "big", signed=False) if len(payload) >= 32 else 1
|
||||||
|
return [
|
||||||
|
{"category": "PT变化", "value": float(pt_ratio or 1), "enabled": bool(pt_ratio)},
|
||||||
|
{"category": "CT变化", "value": float(ct_ratio or 1), "enabled": bool(ct_ratio)},
|
||||||
|
]
|
||||||
|
|
||||||
|
def _decode_line_alarm_setting(self, line_no: int, registers: Sequence[int]) -> Dict[str, Any]:
|
||||||
|
payload = self._registers_to_bytes(registers)
|
||||||
|
categories = ["电压", "电流", "差流", "功率", "频率", "PT断线", "CT断线"]
|
||||||
|
over_limit_alarm: List[Dict[str, Any]] = []
|
||||||
|
fault_alarm: List[Dict[str, Any]] = []
|
||||||
|
for index, category in enumerate(categories):
|
||||||
|
start = index * 4
|
||||||
|
chunk = payload[start : start + 4]
|
||||||
|
if len(chunk) < 4:
|
||||||
|
chunk = chunk + b"\x00" * (4 - len(chunk))
|
||||||
|
rule = {
|
||||||
|
"category": category,
|
||||||
|
"limit": float(chunk[0]),
|
||||||
|
"delay": int(chunk[1]),
|
||||||
|
"output_node": self._decode_output_node(chunk[2]),
|
||||||
|
"enabled": bool(chunk[3]),
|
||||||
|
}
|
||||||
|
if category in {"PT断线", "CT断线"}:
|
||||||
|
fault_alarm.append(rule)
|
||||||
|
else:
|
||||||
|
over_limit_alarm.append(rule)
|
||||||
|
return {
|
||||||
|
"line_no": line_no,
|
||||||
|
"over_limit_alarm": over_limit_alarm,
|
||||||
|
"fault_alarm": fault_alarm,
|
||||||
|
"transformer_change": self._decode_transformer_items(payload),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _read_line_alarm_registers(self, line_no: int) -> List[int]:
|
||||||
|
start_register = LINE_ALARM_START_REGISTER + max(0, line_no - 1) * LINE_ALARM_REGISTERS_PER_LINE
|
||||||
|
return self.transport.read_holding_registers(start_register, LINE_ALARM_REGISTERS_PER_LINE)
|
||||||
|
|
||||||
def read_realtime_data(self) -> RealtimeData:
|
def read_realtime_data(self) -> RealtimeData:
|
||||||
line_list = [
|
line_list = [
|
||||||
self._read_line_data(line_no=index + 1, start_register=start_register)
|
self._read_line_data(line_no=index + 1, start_register=start_register)
|
||||||
@ -515,37 +760,94 @@ class CDeviceClient(DeviceClient):
|
|||||||
|
|
||||||
def read_alarm_events(self) -> List[AlarmEvent]:
|
def read_alarm_events(self) -> List[AlarmEvent]:
|
||||||
event_flag = self.transport.read_input_registers(EVENT_FLAG_REGISTER, 1)[0]
|
event_flag = self.transport.read_input_registers(EVENT_FLAG_REGISTER, 1)[0]
|
||||||
if event_flag == 0:
|
event_count = int(event_flag)
|
||||||
|
if event_count <= 0:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
registers = self.transport.read_holding_registers(EVENT_HOLDING_START_REGISTER, 4)
|
registers = self.transport.read_input_registers(EVENT_INPUT_START_REGISTER, event_count * 5)
|
||||||
signature = tuple(int(value) for value in registers)
|
alarms: List[AlarmEvent] = []
|
||||||
if signature == self._last_event_signature:
|
for index in range(event_count):
|
||||||
return []
|
start = index * 5
|
||||||
self._last_event_signature = signature
|
event_registers = registers[start : start + 5]
|
||||||
|
if len(event_registers) < 5:
|
||||||
|
continue
|
||||||
|
signature = tuple(int(value) for value in event_registers[:4])
|
||||||
|
if signature == self._last_event_signature:
|
||||||
|
continue
|
||||||
|
self._last_event_signature = signature
|
||||||
|
|
||||||
timestamp = int(registers[0]) | (int(registers[1]) << 16)
|
timestamp = int(event_registers[0]) | (int(event_registers[1]) << 16)
|
||||||
milliseconds = int(registers[2]) & 0xFFFF
|
milliseconds = int(event_registers[2]) & 0xFFFF
|
||||||
line_and_type = int(registers[3]) & 0xFFFF
|
encoded = int(event_registers[3]) & 0xFFFF
|
||||||
line_no = (line_and_type >> 8) & 0xFF
|
action_value = int(event_registers[4]) & 0xFFFF
|
||||||
event_type = line_and_type & 0xFF
|
line_no = (encoded >> 12) & 0x0F
|
||||||
|
event_type = (encoded >> 8) & 0x0F
|
||||||
|
event_code = encoded & 0xFF
|
||||||
|
|
||||||
event_name = {
|
event_name = self._decode_event_name(event_type, event_code)
|
||||||
1: "PT断线",
|
event_time = datetime.fromtimestamp(timestamp).replace(microsecond=min(milliseconds, 999) * 1000)
|
||||||
2: "CT断线",
|
line_text = "装置" if line_no == 0 else f"线路{line_no}"
|
||||||
}.get(event_type, f"事件{event_type}")
|
alarms.append(
|
||||||
event_time = datetime.fromtimestamp(timestamp).replace(microsecond=min(milliseconds, 999) * 1000)
|
AlarmEvent(
|
||||||
line_text = "总路" if line_no == 0 else f"线路{line_no}"
|
alarm_type="modbus_event",
|
||||||
return [
|
time=event_time,
|
||||||
AlarmEvent(
|
no="" if line_no == 0 else str(line_no),
|
||||||
alarm_type="modbus_event",
|
type=event_name,
|
||||||
time=event_time,
|
content=f"{line_text}{event_name},动作值={action_value}",
|
||||||
no="" if line_no == 0 else str(line_no),
|
level=self._decode_event_level(event_type),
|
||||||
type=event_name,
|
)
|
||||||
content=f"{line_text}{event_name}",
|
|
||||||
level="中",
|
|
||||||
)
|
)
|
||||||
]
|
|
||||||
|
if alarms:
|
||||||
|
# 协议文档将“读事件成功回复”描述为 03 读取保持寄存器,但该流程需要携带成功个数;
|
||||||
|
# 这里通过写保持寄存器把已成功读取的事件个数回告给设备。
|
||||||
|
self.transport.write_registers(EVENT_SUCCESS_REPLY_REGISTER, [len(alarms)])
|
||||||
|
return alarms
|
||||||
|
|
||||||
|
def _decode_event_name(self, event_type: int, event_code: int) -> str:
|
||||||
|
if event_type == 1:
|
||||||
|
return {1: "PT断线", 2: "CT断线"}.get(event_code, f"保护事件{event_code}")
|
||||||
|
if event_type == 2:
|
||||||
|
return f"动作事件{event_code}"
|
||||||
|
if event_type == 3:
|
||||||
|
return {1: "过压", 2: "过流", 3: "频率越限"}.get(event_code, f"报警事件{event_code}")
|
||||||
|
if event_type == 4:
|
||||||
|
return f"操作事件{event_code}"
|
||||||
|
if event_type == 5:
|
||||||
|
return f"AI越限{event_code}"
|
||||||
|
return f"事件{event_type}-{event_code}"
|
||||||
|
|
||||||
|
def _decode_event_level(self, event_type: int) -> str:
|
||||||
|
return {1: "严重", 2: "中", 3: "中", 4: "一般", 5: "一般"}.get(event_type, "一般")
|
||||||
|
|
||||||
|
def read_channel_config(self) -> Dict[str, Any]:
|
||||||
|
ai_registers = self.transport.read_holding_registers(AI_CHANNEL_START_REGISTER, ANALOG_BLOCK_REGISTER_COUNT)
|
||||||
|
ao_registers = self.transport.read_holding_registers(AO_CHANNEL_START_REGISTER, ANALOG_BLOCK_REGISTER_COUNT)
|
||||||
|
return {
|
||||||
|
"ai_channel": self._decode_channel_items(ai_registers),
|
||||||
|
"ao_channel": self._decode_channel_items(ao_registers),
|
||||||
|
}
|
||||||
|
|
||||||
|
def read_line_alarm_setting(self, line_no: int) -> Dict[str, Any]:
|
||||||
|
return self._decode_line_alarm_setting(line_no, self._read_line_alarm_registers(line_no))
|
||||||
|
|
||||||
|
def read_ai_alarm_setting(self) -> List[Dict[str, Any]]:
|
||||||
|
registers = self.transport.read_holding_registers(AI_ALARM_START_REGISTER, ANALOG_BLOCK_REGISTER_COUNT)
|
||||||
|
return self._decode_ai_alarm_items(registers)
|
||||||
|
|
||||||
|
def read_system_config(self) -> Dict[str, Any]:
|
||||||
|
registers = self.transport.read_holding_registers(TIME_SYNC_START_REGISTER, 2)
|
||||||
|
timestamp = int(registers[0]) | (int(registers[1]) << 16)
|
||||||
|
return {
|
||||||
|
"time_sync": datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
"brightness": SystemConfigIn().brightness,
|
||||||
|
"screen_saver": SystemConfigIn().screen_saver,
|
||||||
|
}
|
||||||
|
|
||||||
|
def read_time_sync_config(self) -> Dict[str, str]:
|
||||||
|
registers = self.transport.read_holding_registers(TIME_SYNC_START_REGISTER, 2)
|
||||||
|
timestamp = int(registers[0]) | (int(registers[1]) << 16)
|
||||||
|
return {"time_sync": datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")}
|
||||||
|
|
||||||
def send_device_config(self, payload: DeviceConfigIn) -> Dict[str, Any]:
|
def send_device_config(self, payload: DeviceConfigIn) -> Dict[str, Any]:
|
||||||
uart_map = {item.port.upper(): item for item in payload.uart}
|
uart_map = {item.port.upper(): item for item in payload.uart}
|
||||||
@ -569,29 +871,8 @@ class CDeviceClient(DeviceClient):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def send_channel_config(self, payload: ChannelConfigIn) -> Dict[str, Any]:
|
def send_channel_config(self, payload: ChannelConfigIn) -> Dict[str, Any]:
|
||||||
def build_channel_payload(items: List[Any]) -> List[int]:
|
ai_registers = self._bytes_to_registers(self._channel_payload_bytes(payload.ai_channel))
|
||||||
raw = bytearray()
|
ao_registers = self._bytes_to_registers(self._channel_payload_bytes(payload.ao_channel))
|
||||||
channel_map = {int(item.ch): item for item in items}
|
|
||||||
for channel_no in range(1, 13):
|
|
||||||
item = channel_map.get(channel_no)
|
|
||||||
if item is None:
|
|
||||||
raw.extend(b"\x00" * 5)
|
|
||||||
continue
|
|
||||||
raw.extend(
|
|
||||||
bytes(
|
|
||||||
[
|
|
||||||
self._signal_type_code(item.singal_type),
|
|
||||||
_clamp_byte(item.line_no),
|
|
||||||
self._measure_type_code(item.type),
|
|
||||||
_clamp_byte(item.limit_high),
|
|
||||||
_clamp_byte(item.limit_low),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return self._bytes_to_registers(bytes(raw))
|
|
||||||
|
|
||||||
ai_registers = build_channel_payload(payload.ai_channel)
|
|
||||||
ao_registers = build_channel_payload(payload.ao_channel)
|
|
||||||
self.transport.write_registers(AI_CHANNEL_START_REGISTER, ai_registers)
|
self.transport.write_registers(AI_CHANNEL_START_REGISTER, ai_registers)
|
||||||
self.transport.write_registers(AO_CHANNEL_START_REGISTER, ao_registers)
|
self.transport.write_registers(AO_CHANNEL_START_REGISTER, ao_registers)
|
||||||
return {
|
return {
|
||||||
@ -606,6 +887,7 @@ class CDeviceClient(DeviceClient):
|
|||||||
def send_line_alarm_setting(self, payload: LineAlarmSettingIn) -> Dict[str, Any]:
|
def send_line_alarm_setting(self, payload: LineAlarmSettingIn) -> Dict[str, Any]:
|
||||||
over_limit_map = {rule.category: rule for rule in payload.over_limit_alarm}
|
over_limit_map = {rule.category: rule for rule in payload.over_limit_alarm}
|
||||||
fault_map = {rule.category: rule for rule in payload.fault_alarm}
|
fault_map = {rule.category: rule for rule in payload.fault_alarm}
|
||||||
|
transformer_map = {item.category: item for item in payload.transformer_change}
|
||||||
|
|
||||||
ordered_rules = [
|
ordered_rules = [
|
||||||
over_limit_map.get("电压"),
|
over_limit_map.get("电压"),
|
||||||
@ -624,6 +906,11 @@ class CDeviceClient(DeviceClient):
|
|||||||
continue
|
continue
|
||||||
raw.extend(self._line_rule_payload(rule.limit, rule.delay, rule.output_node, rule.enabled))
|
raw.extend(self._line_rule_payload(rule.limit, rule.delay, rule.output_node, rule.enabled))
|
||||||
|
|
||||||
|
pt_item = transformer_map.get("PT变化") or transformer_map.get("PT变比")
|
||||||
|
ct_item = transformer_map.get("CT变化") or transformer_map.get("CT变比")
|
||||||
|
raw.extend(self._encode_transformer_value(getattr(pt_item, "value", 1.0)).to_bytes(2, "big"))
|
||||||
|
raw.extend(self._encode_transformer_value(getattr(ct_item, "value", 1.0)).to_bytes(2, "big"))
|
||||||
|
|
||||||
start_register = LINE_ALARM_START_REGISTER + max(0, payload.line_no - 1) * LINE_ALARM_REGISTERS_PER_LINE
|
start_register = LINE_ALARM_START_REGISTER + max(0, payload.line_no - 1) * LINE_ALARM_REGISTERS_PER_LINE
|
||||||
registers = self._bytes_to_registers(bytes(raw))
|
registers = self._bytes_to_registers(bytes(raw))
|
||||||
self.transport.write_registers(start_register, registers)
|
self.transport.write_registers(start_register, registers)
|
||||||
@ -681,6 +968,17 @@ class CDeviceClient(DeviceClient):
|
|||||||
"ignored_fields": ["brightness", "screen_saver"],
|
"ignored_fields": ["brightness", "screen_saver"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def send_time_sync_config(self, payload: TimeSyncConfigIn) -> Dict[str, Any]:
|
||||||
|
timestamp = int(datetime.fromisoformat(payload.time_sync.strip()).timestamp())
|
||||||
|
registers = [timestamp & 0xFFFF, (timestamp >> 16) & 0xFFFF]
|
||||||
|
self.transport.write_registers(TIME_SYNC_START_REGISTER, registers)
|
||||||
|
return {
|
||||||
|
"send_status": "成功",
|
||||||
|
"target": "time_sync",
|
||||||
|
"time_sync": datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
"modbus_written": {"time_registers": len(registers)},
|
||||||
|
}
|
||||||
|
|
||||||
def send_switch_control(self, payload: SwitchControlIn) -> Dict[str, Any]:
|
def send_switch_control(self, payload: SwitchControlIn) -> Dict[str, Any]:
|
||||||
coil_address = max(0, payload.ch - 1)
|
coil_address = max(0, payload.ch - 1)
|
||||||
coil_value = bool(payload.action)
|
coil_value = bool(payload.action)
|
||||||
|
|||||||
@ -4,7 +4,7 @@ from fastapi import APIRouter, Depends, Query
|
|||||||
|
|
||||||
from app.core.response import success_response
|
from app.core.response import success_response
|
||||||
from app.core.security import verify_api_token
|
from app.core.security import verify_api_token
|
||||||
from app.schemas.platform import AiAlarmSettingIn, ChannelConfigIn, DeviceConfigIn, DevicePasswordUpdateIn, LineAlarmSettingIn, NetConfigItem, SystemConfigIn, UartConfigItem
|
from app.schemas.platform import AiAlarmSettingIn, ChannelConfigIn, DeviceConfigIn, DevicePasswordUpdateIn, LineAlarmSettingIn, NetConfigItem, SystemConfigIn, TimeSyncConfigIn, UartConfigItem
|
||||||
from app.services.platform_service import platform_service
|
from app.services.platform_service import platform_service
|
||||||
|
|
||||||
|
|
||||||
@ -84,3 +84,13 @@ def get_system_config() -> Dict[str, Any]:
|
|||||||
@router.post("/system", dependencies=[Depends(verify_api_token)])
|
@router.post("/system", dependencies=[Depends(verify_api_token)])
|
||||||
def save_system_config(payload: SystemConfigIn) -> Dict[str, Any]:
|
def save_system_config(payload: SystemConfigIn) -> Dict[str, Any]:
|
||||||
return success_response(platform_service.save_system_config(payload))
|
return success_response(platform_service.save_system_config(payload))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/time-sync", dependencies=[Depends(verify_api_token)])
|
||||||
|
def get_time_sync_config() -> Dict[str, Any]:
|
||||||
|
return success_response(platform_service.get_time_sync_config(), msg="获取对时设置成功")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/time-sync", dependencies=[Depends(verify_api_token)])
|
||||||
|
def save_time_sync_config(payload: TimeSyncConfigIn) -> Dict[str, Any]:
|
||||||
|
return success_response(platform_service.save_time_sync_config(payload), msg="设备对时成功")
|
||||||
|
|||||||
@ -12,7 +12,9 @@ class Settings(BaseSettings):
|
|||||||
api_prefix: str = "/api"
|
api_prefix: str = "/api"
|
||||||
realtime_ws_path: str = "/ws/real-time"
|
realtime_ws_path: str = "/ws/real-time"
|
||||||
alarm_ws_path: str = "/ws/alarm"
|
alarm_ws_path: str = "/ws/alarm"
|
||||||
|
time_sync_ws_path: str = "/ws/time-sync"
|
||||||
poll_interval_seconds: float = 0.5
|
poll_interval_seconds: float = 0.5
|
||||||
|
time_sync_poll_interval_seconds: float = 1.0
|
||||||
auth_password: str = "admin123"
|
auth_password: str = "admin123"
|
||||||
use_mock_device: bool = True
|
use_mock_device: bool = True
|
||||||
modbus_transport: str = "rtu"
|
modbus_transport: str = "rtu"
|
||||||
@ -34,7 +36,7 @@ class Settings(BaseSettings):
|
|||||||
|
|
||||||
model_config = SettingsConfigDict(
|
model_config = SettingsConfigDict(
|
||||||
env_prefix="EMCP_",
|
env_prefix="EMCP_",
|
||||||
env_file=".env",
|
env_file=BASE_DIR / ".env",
|
||||||
env_file_encoding="utf-8",
|
env_file_encoding="utf-8",
|
||||||
extra="ignore",
|
extra="ignore",
|
||||||
)
|
)
|
||||||
|
|||||||
@ -9,7 +9,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
|||||||
from app.api.router import api_router
|
from app.api.router import api_router
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.db.sqlite import init_db
|
from app.db.sqlite import init_db
|
||||||
from app.tasks.polling import device_polling_loop, stop_task
|
from app.tasks.polling import device_polling_loop, stop_task, time_sync_polling_loop
|
||||||
from app.ws.manager import ws_manager
|
from app.ws.manager import ws_manager
|
||||||
|
|
||||||
|
|
||||||
@ -17,10 +17,12 @@ from app.ws.manager import ws_manager
|
|||||||
async def lifespan(_: FastAPI):
|
async def lifespan(_: FastAPI):
|
||||||
init_db()
|
init_db()
|
||||||
poll_task = asyncio.create_task(device_polling_loop())
|
poll_task = asyncio.create_task(device_polling_loop())
|
||||||
|
time_sync_task = asyncio.create_task(time_sync_polling_loop())
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
finally:
|
finally:
|
||||||
await stop_task(poll_task)
|
await stop_task(poll_task)
|
||||||
|
await stop_task(time_sync_task)
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
@ -63,3 +65,13 @@ async def alarm_ws(websocket: WebSocket) -> None:
|
|||||||
await websocket.receive_text()
|
await websocket.receive_text()
|
||||||
except WebSocketDisconnect:
|
except WebSocketDisconnect:
|
||||||
ws_manager.disconnect("alarm", websocket)
|
ws_manager.disconnect("alarm", websocket)
|
||||||
|
|
||||||
|
|
||||||
|
@app.websocket(settings.time_sync_ws_path)
|
||||||
|
async def time_sync_ws(websocket: WebSocket) -> None:
|
||||||
|
await ws_manager.connect("time-sync", websocket)
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
await websocket.receive_text()
|
||||||
|
except WebSocketDisconnect:
|
||||||
|
ws_manager.disconnect("time-sync", websocket)
|
||||||
|
|||||||
@ -146,6 +146,15 @@ class SystemConfigIn(BaseModel):
|
|||||||
screen_saver: int = Field(default=60, ge=0)
|
screen_saver: int = Field(default=60, ge=0)
|
||||||
|
|
||||||
|
|
||||||
|
class TimeSyncConfigIn(BaseModel):
|
||||||
|
time_sync: str
|
||||||
|
|
||||||
|
|
||||||
|
class TimeSyncEnvelope(BaseModel):
|
||||||
|
type: str = "time_sync"
|
||||||
|
data: Dict[str, str]
|
||||||
|
|
||||||
|
|
||||||
class PasswordVerifyIn(BaseModel):
|
class PasswordVerifyIn(BaseModel):
|
||||||
password: str
|
password: str
|
||||||
|
|
||||||
|
|||||||
@ -21,6 +21,8 @@ from app.schemas.platform import (
|
|||||||
RealtimeData,
|
RealtimeData,
|
||||||
SwitchControlIn,
|
SwitchControlIn,
|
||||||
SystemConfigIn,
|
SystemConfigIn,
|
||||||
|
TimeSyncConfigIn,
|
||||||
|
ValueGroup,
|
||||||
ValueGroup,
|
ValueGroup,
|
||||||
UartConfigItem,
|
UartConfigItem,
|
||||||
)
|
)
|
||||||
@ -43,46 +45,42 @@ class PlatformService:
|
|||||||
return float(primary) / float(secondary)
|
return float(primary) / float(secondary)
|
||||||
|
|
||||||
def _get_config_transformer_ratios(self, line_no: int) -> Dict[str, float]:
|
def _get_config_transformer_ratios(self, line_no: int) -> Dict[str, float]:
|
||||||
"""从配置文件读取 PT/CT 变比。
|
"""从设备参数区读取 PT/CT 变比。
|
||||||
|
|
||||||
来源:setting.json -> line_alarm_setting[].transformer_change[]
|
来源:设备线路定值回读结果中的 transformer_change[]
|
||||||
当前约定:category 为“PT变化/CT变化”(兼容旧的“PT变比/CT变比”),取 value(兼容旧 limit)字段。
|
当前约定:category 为“PT变化/CT变化”(兼容旧的“PT变比/CT变比”),取 value 字段。
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
current = self.config_repo.read_setting_section("line_alarm_setting")
|
current = self.device_client.read_line_alarm_setting(line_no)
|
||||||
except Exception:
|
except Exception:
|
||||||
current = None
|
return {}
|
||||||
line_alarm_list = self._normalize_line_alarm_settings(current)
|
|
||||||
for item in line_alarm_list:
|
transformer_list = current.get("transformer_change")
|
||||||
if item.get("line_no") != line_no:
|
if not isinstance(transformer_list, list):
|
||||||
|
return {}
|
||||||
|
pt_ratio: Optional[float] = None
|
||||||
|
ct_ratio: Optional[float] = None
|
||||||
|
for transformer in transformer_list:
|
||||||
|
if not isinstance(transformer, dict):
|
||||||
continue
|
continue
|
||||||
transformer_list = item.get("transformer_change")
|
category = str(transformer.get("category", "")).strip()
|
||||||
if not isinstance(transformer_list, list):
|
raw_value = transformer.get("value", transformer.get("limit", 1.0))
|
||||||
return {}
|
try:
|
||||||
pt_ratio: Optional[float] = None
|
value = float(raw_value)
|
||||||
ct_ratio: Optional[float] = None
|
except (TypeError, ValueError):
|
||||||
for transformer in transformer_list:
|
value = 1.0
|
||||||
if not isinstance(transformer, dict):
|
if value <= 0:
|
||||||
continue
|
value = 1.0
|
||||||
category = str(transformer.get("category", "")).strip()
|
if category in {"PT变化", "PT变比"}:
|
||||||
raw_value = transformer.get("value", transformer.get("limit", 1.0))
|
pt_ratio = value
|
||||||
try:
|
if category in {"CT变化", "CT变比"}:
|
||||||
value = float(raw_value)
|
ct_ratio = value
|
||||||
except (TypeError, ValueError):
|
result: Dict[str, float] = {}
|
||||||
value = 1.0
|
if pt_ratio is not None:
|
||||||
if value <= 0:
|
result["pt_ratio"] = pt_ratio
|
||||||
value = 1.0
|
if ct_ratio is not None:
|
||||||
if category in {"PT变化", "PT变比"}:
|
result["ct_ratio"] = ct_ratio
|
||||||
pt_ratio = value
|
return result
|
||||||
if category in {"CT变化", "CT变比"}:
|
|
||||||
ct_ratio = value
|
|
||||||
result: Dict[str, float] = {}
|
|
||||||
if pt_ratio is not None:
|
|
||||||
result["pt_ratio"] = pt_ratio
|
|
||||||
if ct_ratio is not None:
|
|
||||||
result["ct_ratio"] = ct_ratio
|
|
||||||
return result
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def _get_line_transformer_ratios(self, line: LineData) -> Dict[str, float]:
|
def _get_line_transformer_ratios(self, line: LineData) -> Dict[str, float]:
|
||||||
"""确定当前线路使用的 PT/CT 变比。
|
"""确定当前线路使用的 PT/CT 变比。
|
||||||
@ -378,18 +376,11 @@ class PlatformService:
|
|||||||
return {"save_path": f"/config/{path.name}", "target": "uart", "port": payload.port, "send_status": "成功"}
|
return {"save_path": f"/config/{path.name}", "target": "uart", "port": payload.port, "send_status": "成功"}
|
||||||
|
|
||||||
def save_channel_config(self, payload: ChannelConfigIn) -> Dict[str, Any]:
|
def save_channel_config(self, payload: ChannelConfigIn) -> Dict[str, Any]:
|
||||||
path = self.config_repo.write_channel_config(payload.model_dump())
|
|
||||||
device_result = self.device_client.send_channel_config(payload)
|
device_result = self.device_client.send_channel_config(payload)
|
||||||
return {"save_path": f"/config/{path.name}", **device_result}
|
return device_result
|
||||||
|
|
||||||
def get_channel_config(self) -> Dict[str, Any]:
|
def get_channel_config(self) -> Dict[str, Any]:
|
||||||
current = self.config_repo.read_channel_config()
|
return self.device_client.read_channel_config()
|
||||||
data = {
|
|
||||||
"ai_channel": [{"ch": 1, "singal_type": "4-20mA", "line_no": 1, "type": "UA", "limit_low": 0, "limit_high": 20}],
|
|
||||||
"ao_channel": [{"ch": 1, "singal_type": "1-5v", "line_no": 2, "type": "UA", "limit_low": 0, "limit_high": 20}],
|
|
||||||
}
|
|
||||||
data.update(current)
|
|
||||||
return data
|
|
||||||
|
|
||||||
def _default_line_alarm_setting(self, line_no: int) -> Dict[str, Any]:
|
def _default_line_alarm_setting(self, line_no: int) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
@ -398,9 +389,13 @@ class PlatformService:
|
|||||||
{"category": "电压", "limit": 180, "delay": 180, "output_node": "开出1", "enabled": True},
|
{"category": "电压", "limit": 180, "delay": 180, "output_node": "开出1", "enabled": True},
|
||||||
{"category": "电流", "limit": 180, "delay": 180, "output_node": "开出1", "enabled": True},
|
{"category": "电流", "limit": 180, "delay": 180, "output_node": "开出1", "enabled": True},
|
||||||
{"category": "差流", "limit": 180, "delay": 180, "output_node": "开出1", "enabled": False},
|
{"category": "差流", "limit": 180, "delay": 180, "output_node": "开出1", "enabled": False},
|
||||||
|
{"category": "功率", "limit": 180, "delay": 180, "output_node": "开出1", "enabled": False},
|
||||||
{"category": "频率", "limit": 180, "delay": 180, "output_node": "开出1", "enabled": False},
|
{"category": "频率", "limit": 180, "delay": 180, "output_node": "开出1", "enabled": False},
|
||||||
],
|
],
|
||||||
"fault_alarm": [{"category": "PT断线", "delay": 180, "output_node": "开出1", "enabled": True}],
|
"fault_alarm": [
|
||||||
|
{"category": "PT断线", "limit": 0, "delay": 180, "output_node": "开出1", "enabled": True},
|
||||||
|
{"category": "CT断线", "limit": 0, "delay": 180, "output_node": "开出2", "enabled": False},
|
||||||
|
],
|
||||||
"transformer_change": [
|
"transformer_change": [
|
||||||
{"category": "PT变化", "value": 1.0, "enabled": False},
|
{"category": "PT变化", "value": 1.0, "enabled": False},
|
||||||
{"category": "CT变化", "value": 1.0, "enabled": False},
|
{"category": "CT变化", "value": 1.0, "enabled": False},
|
||||||
@ -415,103 +410,47 @@ class PlatformService:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
def save_line_alarm_setting(self, payload: LineAlarmSettingIn) -> Dict[str, Any]:
|
def save_line_alarm_setting(self, payload: LineAlarmSettingIn) -> Dict[str, Any]:
|
||||||
current = self.config_repo.read_json("setting.json")
|
|
||||||
if not isinstance(current, dict):
|
|
||||||
current = {}
|
|
||||||
line_alarm_list = self._normalize_line_alarm_settings(current.get("line_alarm_setting"))
|
|
||||||
payload_data = payload.model_dump(exclude_unset=True)
|
|
||||||
payload_data["line_no"] = payload.line_no
|
|
||||||
|
|
||||||
updated = False
|
|
||||||
for index, item in enumerate(line_alarm_list):
|
|
||||||
if item.get("line_no") == payload.line_no:
|
|
||||||
merged = dict(item)
|
|
||||||
merged.update(payload_data)
|
|
||||||
default = self._default_line_alarm_setting(payload.line_no)
|
|
||||||
for key in ("over_limit_alarm", "fault_alarm", "transformer_change"):
|
|
||||||
if not isinstance(merged.get(key), list):
|
|
||||||
merged[key] = default[key]
|
|
||||||
line_alarm_list[index] = merged
|
|
||||||
updated = True
|
|
||||||
break
|
|
||||||
if not updated:
|
|
||||||
merged = self._default_line_alarm_setting(payload.line_no)
|
|
||||||
merged.update(payload_data)
|
|
||||||
for key in ("over_limit_alarm", "fault_alarm", "transformer_change"):
|
|
||||||
if not isinstance(merged.get(key), list):
|
|
||||||
merged[key] = self._default_line_alarm_setting(payload.line_no)[key]
|
|
||||||
line_alarm_list.append(merged)
|
|
||||||
|
|
||||||
current["line_alarm_setting"] = line_alarm_list
|
|
||||||
path = self.config_repo.write_setting_config(current)
|
|
||||||
device_result = self.device_client.send_line_alarm_setting(payload)
|
device_result = self.device_client.send_line_alarm_setting(payload)
|
||||||
return {"save_path": f"/config/{path.name}", **device_result}
|
return device_result
|
||||||
|
|
||||||
def get_line_alarm_setting(self, line_no: int = 1) -> Dict[str, Any]:
|
def get_line_alarm_setting(self, line_no: int = 1) -> Dict[str, Any]:
|
||||||
current = self.config_repo.read_setting_section("line_alarm_setting")
|
current = self.device_client.read_line_alarm_setting(line_no)
|
||||||
line_alarm_list = self._normalize_line_alarm_settings(current)
|
merged = self._default_line_alarm_setting(line_no)
|
||||||
for item in line_alarm_list:
|
if isinstance(current, dict):
|
||||||
if item.get("line_no") == line_no:
|
merged.update(current)
|
||||||
merged = self._default_line_alarm_setting(line_no)
|
normalized_transformer: List[Dict[str, Any]] = []
|
||||||
merged.update(item)
|
for transformer in merged.get("transformer_change", []):
|
||||||
for key in ("over_limit_alarm", "fault_alarm", "transformer_change"):
|
if not isinstance(transformer, dict):
|
||||||
if not isinstance(merged.get(key), list):
|
continue
|
||||||
merged[key] = self._default_line_alarm_setting(line_no)[key]
|
category = transformer.get("category", "")
|
||||||
normalized_transformer: List[Dict[str, Any]] = []
|
enabled = bool(transformer.get("enabled", False))
|
||||||
for transformer in merged.get("transformer_change", []):
|
raw_value = transformer.get("value", transformer.get("limit", 1.0))
|
||||||
if not isinstance(transformer, dict):
|
try:
|
||||||
continue
|
value = float(raw_value)
|
||||||
category = transformer.get("category", "")
|
except (TypeError, ValueError):
|
||||||
enabled = bool(transformer.get("enabled", False))
|
value = 1.0
|
||||||
raw_value = transformer.get("value", transformer.get("limit", 1.0))
|
normalized_transformer.append({"category": category, "value": value, "enabled": enabled})
|
||||||
try:
|
merged["transformer_change"] = normalized_transformer
|
||||||
value = float(raw_value)
|
return merged
|
||||||
except (TypeError, ValueError):
|
|
||||||
value = 1.0
|
|
||||||
normalized_transformer.append({"category": category, "value": value, "enabled": enabled})
|
|
||||||
merged["transformer_change"] = normalized_transformer
|
|
||||||
return merged
|
|
||||||
return self._default_line_alarm_setting(line_no)
|
|
||||||
|
|
||||||
def save_ai_alarm_setting(self, payload: List[AiAlarmSettingIn]) -> Dict[str, Any]:
|
def save_ai_alarm_setting(self, payload: List[AiAlarmSettingIn]) -> Dict[str, Any]:
|
||||||
current = self.config_repo.read_json("setting.json")
|
payload_data = [item.model_dump() for item in payload]
|
||||||
if not isinstance(current, dict):
|
return self.device_client.send_ai_alarm_setting(payload_data)
|
||||||
current = {}
|
|
||||||
current["ai_alarm_setting"] = [item.model_dump() for item in payload]
|
|
||||||
path = self.config_repo.write_setting_config(current)
|
|
||||||
device_result = self.device_client.send_ai_alarm_setting(current["ai_alarm_setting"])
|
|
||||||
return {"save_path": f"/config/{path.name}", **device_result}
|
|
||||||
|
|
||||||
def get_ai_alarm_setting(self) -> List[Dict[str, Any]]:
|
def get_ai_alarm_setting(self) -> List[Dict[str, Any]]:
|
||||||
current = self.config_repo.read_setting_section("ai_alarm_setting")
|
return self.device_client.read_ai_alarm_setting()
|
||||||
if isinstance(current, list):
|
|
||||||
return current
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
"channel_no": 1,
|
|
||||||
"singal_type": "4-20mA",
|
|
||||||
"limit_low": 0,
|
|
||||||
"limit_high": 20,
|
|
||||||
"delay": 180,
|
|
||||||
"output_node": "开出1",
|
|
||||||
"enabled": True,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
def save_system_config(self, payload: SystemConfigIn) -> Dict[str, Any]:
|
def save_system_config(self, payload: SystemConfigIn) -> Dict[str, Any]:
|
||||||
current = self.config_repo.read_json("setting.json")
|
|
||||||
if not isinstance(current, dict):
|
|
||||||
current = {}
|
|
||||||
current["system_config"] = payload.model_dump()
|
|
||||||
self.config_repo.write_setting_config(current)
|
|
||||||
return self.device_client.send_system_config(payload)
|
return self.device_client.send_system_config(payload)
|
||||||
|
|
||||||
def get_system_config(self) -> Dict[str, Any]:
|
def get_system_config(self) -> Dict[str, Any]:
|
||||||
current = self.config_repo.read_setting_section("system_config")
|
return self.device_client.read_system_config()
|
||||||
data = SystemConfigIn().model_dump()
|
|
||||||
if isinstance(current, dict):
|
def save_time_sync_config(self, payload: TimeSyncConfigIn) -> Dict[str, Any]:
|
||||||
data.update(current)
|
return self.device_client.send_time_sync_config(payload)
|
||||||
return data
|
|
||||||
|
def get_time_sync_config(self) -> Dict[str, Any]:
|
||||||
|
return self.device_client.read_time_sync_config()
|
||||||
|
|
||||||
def list_alarms(
|
def list_alarms(
|
||||||
self,
|
self,
|
||||||
@ -534,6 +473,10 @@ class PlatformService:
|
|||||||
def switch_control(self, payload: SwitchControlIn) -> Dict[str, Any]:
|
def switch_control(self, payload: SwitchControlIn) -> Dict[str, Any]:
|
||||||
return self.device_client.send_switch_control(payload)
|
return self.device_client.send_switch_control(payload)
|
||||||
|
|
||||||
|
async def poll_time_sync_once(self) -> None:
|
||||||
|
time_sync = self.device_client.read_time_sync_config()
|
||||||
|
await ws_manager.broadcast("time-sync", {"type": "time_sync", "data": time_sync})
|
||||||
|
|
||||||
async def poll_device_once(self) -> None:
|
async def poll_device_once(self) -> None:
|
||||||
realtime = self._normalize_realtime_precision(self.device_client.read_realtime_data())
|
realtime = self._normalize_realtime_precision(self.device_client.read_realtime_data())
|
||||||
status = self.device_client.read_device_status()
|
status = self.device_client.read_device_status()
|
||||||
|
|||||||
@ -14,6 +14,12 @@ async def device_polling_loop() -> None:
|
|||||||
await asyncio.sleep(settings.poll_interval_seconds)
|
await asyncio.sleep(settings.poll_interval_seconds)
|
||||||
|
|
||||||
|
|
||||||
|
async def time_sync_polling_loop() -> None:
|
||||||
|
while True:
|
||||||
|
await platform_service.poll_time_sync_once()
|
||||||
|
await asyncio.sleep(settings.time_sync_poll_interval_seconds)
|
||||||
|
|
||||||
|
|
||||||
async def stop_task(task: Optional[asyncio.Task]) -> None:
|
async def stop_task(task: Optional[asyncio.Task]) -> None:
|
||||||
if task is None:
|
if task is None:
|
||||||
return
|
return
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"ai_channel": [
|
"ai_channel": [
|
||||||
{
|
{
|
||||||
"ch": 1,
|
"ch": 1,
|
||||||
"singal_type": "1-5v",
|
"singal_type": "4~20mA",
|
||||||
"line_no": 2,
|
"line_no": 2,
|
||||||
"type": "UA",
|
"type": "UA",
|
||||||
"limit_low": 10.0,
|
"limit_low": 10.0,
|
||||||
|
|||||||
@ -334,8 +334,8 @@
|
|||||||
"ai_alarm_setting": [
|
"ai_alarm_setting": [
|
||||||
{
|
{
|
||||||
"channel_no": 1,
|
"channel_no": 1,
|
||||||
"singal_type": "4-20mA",
|
"singal_type": "4~20mA",
|
||||||
"limit_low": 0.0,
|
"limit_low": 4.0,
|
||||||
"limit_high": 20.0,
|
"limit_high": 20.0,
|
||||||
"delay": 180,
|
"delay": 180,
|
||||||
"output_node": "开出1",
|
"output_node": "开出1",
|
||||||
@ -344,8 +344,8 @@
|
|||||||
{
|
{
|
||||||
"channel_no": 2,
|
"channel_no": 2,
|
||||||
"singal_type": "1~5V",
|
"singal_type": "1~5V",
|
||||||
"limit_low": 0.0,
|
"limit_low": 1.0,
|
||||||
"limit_high": 8.0,
|
"limit_high": 5.0,
|
||||||
"delay": 8,
|
"delay": 8,
|
||||||
"output_node": "",
|
"output_node": "",
|
||||||
"enabled": true
|
"enabled": true
|
||||||
@ -353,7 +353,7 @@
|
|||||||
{
|
{
|
||||||
"channel_no": 3,
|
"channel_no": 3,
|
||||||
"singal_type": "4~20mA",
|
"singal_type": "4~20mA",
|
||||||
"limit_low": 0.0,
|
"limit_low": 4.0,
|
||||||
"limit_high": 8.0,
|
"limit_high": 8.0,
|
||||||
"delay": 8,
|
"delay": 8,
|
||||||
"output_node": "",
|
"output_node": "",
|
||||||
@ -362,8 +362,8 @@
|
|||||||
{
|
{
|
||||||
"channel_no": 4,
|
"channel_no": 4,
|
||||||
"singal_type": "1~5V",
|
"singal_type": "1~5V",
|
||||||
"limit_low": 0.0,
|
"limit_low": 1.0,
|
||||||
"limit_high": 8.0,
|
"limit_high": 4.0,
|
||||||
"delay": 8,
|
"delay": 8,
|
||||||
"output_node": "",
|
"output_node": "",
|
||||||
"enabled": false
|
"enabled": false
|
||||||
@ -371,7 +371,7 @@
|
|||||||
{
|
{
|
||||||
"channel_no": 5,
|
"channel_no": 5,
|
||||||
"singal_type": "4~20mA",
|
"singal_type": "4~20mA",
|
||||||
"limit_low": 0.0,
|
"limit_low": 4.0,
|
||||||
"limit_high": 8.0,
|
"limit_high": 8.0,
|
||||||
"delay": 8,
|
"delay": 8,
|
||||||
"output_node": "",
|
"output_node": "",
|
||||||
@ -380,8 +380,8 @@
|
|||||||
{
|
{
|
||||||
"channel_no": 6,
|
"channel_no": 6,
|
||||||
"singal_type": "1~5V",
|
"singal_type": "1~5V",
|
||||||
"limit_low": 0.0,
|
"limit_low": 1.0,
|
||||||
"limit_high": 8.0,
|
"limit_high": 5.0,
|
||||||
"delay": 8,
|
"delay": 8,
|
||||||
"output_node": "",
|
"output_node": "",
|
||||||
"enabled": false
|
"enabled": false
|
||||||
@ -389,7 +389,7 @@
|
|||||||
{
|
{
|
||||||
"channel_no": 7,
|
"channel_no": 7,
|
||||||
"singal_type": "4~20mA",
|
"singal_type": "4~20mA",
|
||||||
"limit_low": 0.0,
|
"limit_low": 4.0,
|
||||||
"limit_high": 8.0,
|
"limit_high": 8.0,
|
||||||
"delay": 8,
|
"delay": 8,
|
||||||
"output_node": "",
|
"output_node": "",
|
||||||
@ -398,8 +398,8 @@
|
|||||||
{
|
{
|
||||||
"channel_no": 8,
|
"channel_no": 8,
|
||||||
"singal_type": "1~5V",
|
"singal_type": "1~5V",
|
||||||
"limit_low": 0.0,
|
"limit_low": 1.0,
|
||||||
"limit_high": 8.0,
|
"limit_high": 5.0,
|
||||||
"delay": 8,
|
"delay": 8,
|
||||||
"output_node": "",
|
"output_node": "",
|
||||||
"enabled": false
|
"enabled": false
|
||||||
@ -407,7 +407,7 @@
|
|||||||
{
|
{
|
||||||
"channel_no": 9,
|
"channel_no": 9,
|
||||||
"singal_type": "4~20mA",
|
"singal_type": "4~20mA",
|
||||||
"limit_low": 0.0,
|
"limit_low": 4.0,
|
||||||
"limit_high": 8.0,
|
"limit_high": 8.0,
|
||||||
"delay": 8,
|
"delay": 8,
|
||||||
"output_node": "",
|
"output_node": "",
|
||||||
@ -416,8 +416,8 @@
|
|||||||
{
|
{
|
||||||
"channel_no": 10,
|
"channel_no": 10,
|
||||||
"singal_type": "1~5V",
|
"singal_type": "1~5V",
|
||||||
"limit_low": 0.0,
|
"limit_low": 1.0,
|
||||||
"limit_high": 8.0,
|
"limit_high": 5.0,
|
||||||
"delay": 8,
|
"delay": 8,
|
||||||
"output_node": "",
|
"output_node": "",
|
||||||
"enabled": false
|
"enabled": false
|
||||||
@ -425,7 +425,7 @@
|
|||||||
{
|
{
|
||||||
"channel_no": 11,
|
"channel_no": 11,
|
||||||
"singal_type": "4~20mA",
|
"singal_type": "4~20mA",
|
||||||
"limit_low": 0.0,
|
"limit_low": 4.0,
|
||||||
"limit_high": 8.0,
|
"limit_high": 8.0,
|
||||||
"delay": 8,
|
"delay": 8,
|
||||||
"output_node": "",
|
"output_node": "",
|
||||||
@ -433,8 +433,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"channel_no": 12,
|
"channel_no": 12,
|
||||||
"singal_type": "1~5V",
|
"singal_type": "4~20mA",
|
||||||
"limit_low": 0.0,
|
"limit_low": 4.0,
|
||||||
"limit_high": 8.0,
|
"limit_high": 8.0,
|
||||||
"delay": 8,
|
"delay": 8,
|
||||||
"output_node": "",
|
"output_node": "",
|
||||||
|
|||||||
@ -2,6 +2,7 @@ from datetime import datetime
|
|||||||
|
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from app.adapters.device_client import MockDeviceClient
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.security import hash_password
|
from app.core.security import hash_password
|
||||||
from app.db.sqlite import get_connection, init_db
|
from app.db.sqlite import get_connection, init_db
|
||||||
@ -130,7 +131,9 @@ def test_alarm_list_filters(tmp_path) -> None:
|
|||||||
|
|
||||||
def test_config_query_endpoints(tmp_path) -> None:
|
def test_config_query_endpoints(tmp_path) -> None:
|
||||||
old_repo = platform_service.config_repo
|
old_repo = platform_service.config_repo
|
||||||
|
old_client = platform_service.device_client
|
||||||
platform_service.config_repo = JsonConfigRepository(tmp_path)
|
platform_service.config_repo = JsonConfigRepository(tmp_path)
|
||||||
|
platform_service.device_client = MockDeviceClient()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
platform_service.config_repo.write_device_config(
|
platform_service.config_repo.write_device_config(
|
||||||
@ -154,22 +157,6 @@ def test_config_query_endpoints(tmp_path) -> None:
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
platform_service.config_repo.write_channel_config(
|
|
||||||
{
|
|
||||||
"ai_channel": [{"ch": 1, "singal_type": "4-20mA", "line_no": 1, "type": "UA", "limit_low": 0, "limit_high": 20}],
|
|
||||||
"ao_channel": [{"ch": 1, "singal_type": "1-5v", "line_no": 2, "type": "UA", "limit_low": 0, "limit_high": 20}],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
platform_service.config_repo.write_setting_config(
|
|
||||||
{
|
|
||||||
"line_alarm_setting": [
|
|
||||||
{"line_no": 1, "over_limit_alarm": [{"category": "电压", "limit": 180, "delay": 180, "output_node": "开出1", "enabled": True}], "fault_alarm": []},
|
|
||||||
{"line_no": 2, "over_limit_alarm": [{"category": "电流", "limit": 200, "delay": 120, "output_node": "开出2", "enabled": True}], "fault_alarm": []},
|
|
||||||
],
|
|
||||||
"ai_alarm_setting": [{"channel_no": 1, "singal_type": "4-20mA", "limit_low": 0, "limit_high": 20, "delay": 180, "output_node": "开出1", "enabled": True}],
|
|
||||||
"system_config": {"time_sync": "manual", "brightness": 83, "screen_saver": 60},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
headers = {"X-API-Token": settings.auth_password}
|
headers = {"X-API-Token": settings.auth_password}
|
||||||
|
|
||||||
@ -177,9 +164,10 @@ def test_config_query_endpoints(tmp_path) -> None:
|
|||||||
net_response = client.get("/api/config/device/net", headers=headers, params={"nic": "网卡一"})
|
net_response = client.get("/api/config/device/net", headers=headers, params={"nic": "网卡一"})
|
||||||
uart_response = client.get("/api/config/device/uart", headers=headers, params={"port": "COM1"})
|
uart_response = client.get("/api/config/device/uart", headers=headers, params={"port": "COM1"})
|
||||||
channel_response = client.get("/api/config/channel", headers=headers)
|
channel_response = client.get("/api/config/channel", headers=headers)
|
||||||
line_alarm_response = client.get("/api/config/line_alarm_setting", headers=headers, params={"line_no": 2})
|
line_alarm_response = client.get("/api/config/line_alarm_setting", headers=headers, params={"line_no": 1})
|
||||||
ai_alarm_response = client.get("/api/config/ai_alarm_setting", headers=headers)
|
ai_alarm_response = client.get("/api/config/ai_alarm_setting", headers=headers)
|
||||||
system_response = client.get("/api/config/system", headers=headers)
|
system_response = client.get("/api/config/system", headers=headers)
|
||||||
|
time_sync_response = client.get("/api/config/time-sync", headers=headers)
|
||||||
|
|
||||||
assert device_response.status_code == 200
|
assert device_response.status_code == 200
|
||||||
assert net_response.status_code == 200
|
assert net_response.status_code == 200
|
||||||
@ -188,44 +176,93 @@ def test_config_query_endpoints(tmp_path) -> None:
|
|||||||
assert line_alarm_response.status_code == 200
|
assert line_alarm_response.status_code == 200
|
||||||
assert ai_alarm_response.status_code == 200
|
assert ai_alarm_response.status_code == 200
|
||||||
assert system_response.status_code == 200
|
assert system_response.status_code == 200
|
||||||
|
assert time_sync_response.status_code == 200
|
||||||
|
|
||||||
assert device_response.json()["data"]["password"] == ""
|
assert device_response.json()["data"]["password"] == ""
|
||||||
assert device_response.json()["data"]["net"][0]["ip"] == "192.168.1.10"
|
assert device_response.json()["data"]["net"][0]["ip"] == "192.168.1.10"
|
||||||
assert net_response.json()["data"]["nic"] == "网卡一"
|
assert net_response.json()["data"]["nic"] == "网卡一"
|
||||||
assert uart_response.json()["data"]["port"] == "COM1"
|
assert uart_response.json()["data"]["port"] == "COM1"
|
||||||
assert channel_response.json()["data"]["ai_channel"][0]["singal_type"] == "4-20mA"
|
assert channel_response.json()["data"]["ai_channel"][0]["singal_type"] == "4-20mA"
|
||||||
assert line_alarm_response.json()["data"]["line_no"] == 2
|
assert line_alarm_response.json()["data"]["line_no"] == 1
|
||||||
assert line_alarm_response.json()["data"]["over_limit_alarm"][0]["category"] == "电流"
|
assert line_alarm_response.json()["data"]["transformer_change"][0]["category"] == "PT变化"
|
||||||
assert ai_alarm_response.json()["data"][0]["channel_no"] == 1
|
assert ai_alarm_response.json()["data"][0]["channel_no"] == 1
|
||||||
assert system_response.json()["data"]["brightness"] == 83
|
assert system_response.json()["data"]["time_sync"] == "2026-05-25 12:34:56"
|
||||||
|
assert time_sync_response.json()["data"]["time_sync"] == "2026-05-25 12:34:56"
|
||||||
finally:
|
finally:
|
||||||
|
platform_service.device_client = old_client
|
||||||
platform_service.config_repo = old_repo
|
platform_service.config_repo = old_repo
|
||||||
|
|
||||||
|
|
||||||
def test_save_line_alarm_setting_stores_list(tmp_path) -> None:
|
def test_protocol_config_endpoints_read_and_write_device_only(tmp_path) -> None:
|
||||||
old_repo = platform_service.config_repo
|
old_repo = platform_service.config_repo
|
||||||
|
old_client = platform_service.device_client
|
||||||
platform_service.config_repo = JsonConfigRepository(tmp_path)
|
platform_service.config_repo = JsonConfigRepository(tmp_path)
|
||||||
|
platform_service.device_client = MockDeviceClient()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
headers = {"X-API-Token": settings.auth_password}
|
headers = {"X-API-Token": settings.auth_password}
|
||||||
response = client.post(
|
channel_response = client.post(
|
||||||
|
"/api/config/channel",
|
||||||
|
headers=headers,
|
||||||
|
json={
|
||||||
|
"ai_channel": [{"ch": 1, "singal_type": "4-20mA", "line_no": 3, "type": "UA", "limit_low": 0, "limit_high": 20}],
|
||||||
|
"ao_channel": [{"ch": 2, "singal_type": "1~5V", "line_no": 2, "type": "Q", "limit_low": 1, "limit_high": 8}],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
line_response = client.post(
|
||||||
"/api/config/line_alarm_setting",
|
"/api/config/line_alarm_setting",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
json={
|
json={
|
||||||
"line_no": 3,
|
"line_no": 3,
|
||||||
"over_limit_alarm": [{"category": "频率", "limit": 51.5, "delay": 30, "output_node": "开出1", "enabled": True}],
|
"over_limit_alarm": [{"category": "频率", "limit": 51.5, "delay": 30, "output_node": "开出1", "enabled": True}],
|
||||||
"fault_alarm": [{"category": "PT断线", "delay": 20, "output_node": "开出2", "enabled": False}],
|
"fault_alarm": [{"category": "PT断线", "delay": 20, "output_node": "开出2", "enabled": False}],
|
||||||
|
"transformer_change": [{"category": "PT变化", "value": 20, "enabled": True}, {"category": "CT变化", "value": 30, "enabled": True}],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
ai_alarm_response = client.post(
|
||||||
|
"/api/config/ai_alarm_setting",
|
||||||
|
headers=headers,
|
||||||
|
json=[
|
||||||
|
{
|
||||||
|
"channel_no": 1,
|
||||||
|
"singal_type": "4-20mA",
|
||||||
|
"limit_low": 0,
|
||||||
|
"limit_high": 20,
|
||||||
|
"delay": 30,
|
||||||
|
"output_node": "开出1",
|
||||||
|
"enabled": True,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
system_response = client.post(
|
||||||
|
"/api/config/system",
|
||||||
|
headers=headers,
|
||||||
|
json={"time_sync": "2026-05-25 13:00:00", "brightness": 88, "screen_saver": 90},
|
||||||
|
)
|
||||||
|
query_channel = client.get("/api/config/channel", headers=headers)
|
||||||
|
query_line = client.get("/api/config/line_alarm_setting", headers=headers, params={"line_no": 3})
|
||||||
|
query_ai_alarm = client.get("/api/config/ai_alarm_setting", headers=headers)
|
||||||
|
query_system = client.get("/api/config/system", headers=headers)
|
||||||
|
time_sync_response = client.post(
|
||||||
|
"/api/config/time-sync",
|
||||||
|
headers=headers,
|
||||||
|
json={"time_sync": "2026-05-25 14:00:00"},
|
||||||
|
)
|
||||||
|
query_time_sync = client.get("/api/config/time-sync", headers=headers)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert channel_response.status_code == 200
|
||||||
|
assert line_response.status_code == 200
|
||||||
saved = platform_service.config_repo.read_setting_config()
|
assert ai_alarm_response.status_code == 200
|
||||||
assert isinstance(saved["line_alarm_setting"], list)
|
assert system_response.status_code == 200
|
||||||
assert saved["line_alarm_setting"][0]["line_no"] == 3
|
assert time_sync_response.status_code == 200
|
||||||
assert "transformer_change" in saved["line_alarm_setting"][0]
|
assert query_channel.json()["data"]["ai_channel"][0]["line_no"] == 3
|
||||||
assert len(saved["line_alarm_setting"][0]["transformer_change"]) == 2
|
assert query_line.json()["data"]["transformer_change"][0]["value"] == 20.0
|
||||||
|
assert query_ai_alarm.json()["data"][0]["delay"] == 30
|
||||||
|
assert query_system.json()["data"]["brightness"] == 88
|
||||||
|
assert query_time_sync.json()["data"]["time_sync"] == "2026-05-25 14:00:00"
|
||||||
|
assert platform_service.config_repo.read_setting_config() == {}
|
||||||
finally:
|
finally:
|
||||||
|
platform_service.device_client = old_client
|
||||||
platform_service.config_repo = old_repo
|
platform_service.config_repo = old_repo
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -11,9 +11,11 @@ from app.adapters.device_client import (
|
|||||||
AO_CHANNEL_START_REGISTER,
|
AO_CHANNEL_START_REGISTER,
|
||||||
CDeviceClient,
|
CDeviceClient,
|
||||||
DI_INPUT_REGISTER,
|
DI_INPUT_REGISTER,
|
||||||
|
DO_INPUT_REGISTER,
|
||||||
DO_STATE_REGISTER,
|
DO_STATE_REGISTER,
|
||||||
EVENT_FLAG_REGISTER,
|
EVENT_FLAG_REGISTER,
|
||||||
EVENT_HOLDING_START_REGISTER,
|
EVENT_INPUT_START_REGISTER,
|
||||||
|
EVENT_SUCCESS_REPLY_REGISTER,
|
||||||
LINE_ALARM_REGISTERS_PER_LINE,
|
LINE_ALARM_REGISTERS_PER_LINE,
|
||||||
LINE_ALARM_START_REGISTER,
|
LINE_ALARM_START_REGISTER,
|
||||||
LINE_START_REGISTERS,
|
LINE_START_REGISTERS,
|
||||||
@ -35,6 +37,7 @@ from app.schemas.platform import (
|
|||||||
HardwareVersion,
|
HardwareVersion,
|
||||||
SwitchControlIn,
|
SwitchControlIn,
|
||||||
SystemConfigIn,
|
SystemConfigIn,
|
||||||
|
TimeSyncConfigIn,
|
||||||
UartConfigItem,
|
UartConfigItem,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -103,7 +106,7 @@ def test_modbus_client_reads_upstream_data() -> None:
|
|||||||
ai_values.extend(client._float_to_registers(1.25 + index))
|
ai_values.extend(client._float_to_registers(1.25 + index))
|
||||||
transport.set_input_block(AI_INPUT_START_REGISTER, ai_values)
|
transport.set_input_block(AI_INPUT_START_REGISTER, ai_values)
|
||||||
transport.set_discrete_block(0x0000, [True, False, True, False, True, False, True, False, True, False, True, False])
|
transport.set_discrete_block(0x0000, [True, False, True, False, True, False, True, False, True, False, True, False])
|
||||||
transport.set_coil_block(0x0000, [False, True, False, True, False, True, False, True, False, True, False, True])
|
transport.set_input_block(DO_INPUT_REGISTER, [0x0AAA])
|
||||||
transport.set_input_block(DI_INPUT_REGISTER, [0x0555])
|
transport.set_input_block(DI_INPUT_REGISTER, [0x0555])
|
||||||
|
|
||||||
realtime = client.read_realtime_data()
|
realtime = client.read_realtime_data()
|
||||||
@ -127,13 +130,14 @@ def test_modbus_client_reads_alarm_event_once() -> None:
|
|||||||
timestamp = int(datetime(2026, 5, 25, 12, 0, 0).timestamp())
|
timestamp = int(datetime(2026, 5, 25, 12, 0, 0).timestamp())
|
||||||
|
|
||||||
transport.set_input_block(EVENT_FLAG_REGISTER, [1])
|
transport.set_input_block(EVENT_FLAG_REGISTER, [1])
|
||||||
transport.set_holding_block(
|
transport.set_input_block(
|
||||||
EVENT_HOLDING_START_REGISTER,
|
EVENT_INPUT_START_REGISTER,
|
||||||
[
|
[
|
||||||
timestamp & 0xFFFF,
|
timestamp & 0xFFFF,
|
||||||
(timestamp >> 16) & 0xFFFF,
|
(timestamp >> 16) & 0xFFFF,
|
||||||
123,
|
123,
|
||||||
(2 << 8) | 1,
|
(2 << 12) | (1 << 8) | 1,
|
||||||
|
456,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -145,12 +149,58 @@ def test_modbus_client_reads_alarm_event_once() -> None:
|
|||||||
assert alarms[0].type == "PT断线"
|
assert alarms[0].type == "PT断线"
|
||||||
assert "线路2PT断线" in alarms[0].content
|
assert "线路2PT断线" in alarms[0].content
|
||||||
assert duplicate == []
|
assert duplicate == []
|
||||||
|
assert transport.written_registers[-1] == (EVENT_SUCCESS_REPLY_REGISTER, [1])
|
||||||
|
|
||||||
|
|
||||||
def test_modbus_client_writes_downstream_registers() -> None:
|
def test_modbus_client_reads_and_writes_downstream_registers() -> None:
|
||||||
transport = FakeTransport()
|
transport = FakeTransport()
|
||||||
client = CDeviceClient(transport=transport)
|
client = CDeviceClient(transport=transport)
|
||||||
|
|
||||||
|
ai_channel_payload = client._bytes_to_registers(
|
||||||
|
bytes(
|
||||||
|
[
|
||||||
|
0, 1, 0, 20, 0,
|
||||||
|
1, 2, 7, 8, 1,
|
||||||
|
]
|
||||||
|
+ [0] * 50
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ao_channel_payload = client._bytes_to_registers(
|
||||||
|
bytes(
|
||||||
|
[
|
||||||
|
1, 1, 6, 10, 2,
|
||||||
|
]
|
||||||
|
+ [0] * 55
|
||||||
|
)
|
||||||
|
)
|
||||||
|
line_alarm_payload_bytes = bytes(
|
||||||
|
[
|
||||||
|
180, 5, 1, 1,
|
||||||
|
200, 6, 2, 0,
|
||||||
|
20, 7, 3, 1,
|
||||||
|
30, 8, 4, 0,
|
||||||
|
50, 9, 5, 1,
|
||||||
|
0, 10, 6, 1,
|
||||||
|
0, 11, 7, 0,
|
||||||
|
]
|
||||||
|
) + (10).to_bytes(2, "big") + (20).to_bytes(2, "big")
|
||||||
|
ai_alarm_payload_bytes = bytes(
|
||||||
|
[
|
||||||
|
20, 0, 3, 1, 1,
|
||||||
|
]
|
||||||
|
+ [0] * 55
|
||||||
|
)
|
||||||
|
timestamp = int(datetime(2026, 5, 25, 12, 34, 56).timestamp())
|
||||||
|
|
||||||
|
transport.set_holding_block(AI_CHANNEL_START_REGISTER, ai_channel_payload)
|
||||||
|
transport.set_holding_block(AO_CHANNEL_START_REGISTER, ao_channel_payload)
|
||||||
|
transport.set_holding_block(
|
||||||
|
LINE_ALARM_START_REGISTER + LINE_ALARM_REGISTERS_PER_LINE,
|
||||||
|
client._bytes_to_registers(line_alarm_payload_bytes),
|
||||||
|
)
|
||||||
|
transport.set_holding_block(AI_ALARM_START_REGISTER, client._bytes_to_registers(ai_alarm_payload_bytes))
|
||||||
|
transport.set_holding_block(TIME_SYNC_START_REGISTER, [timestamp & 0xFFFF, (timestamp >> 16) & 0xFFFF])
|
||||||
|
|
||||||
device_payload = DeviceConfigIn(
|
device_payload = DeviceConfigIn(
|
||||||
password="",
|
password="",
|
||||||
hardware_version=HardwareVersion(),
|
hardware_version=HardwareVersion(),
|
||||||
@ -193,23 +243,39 @@ def test_modbus_client_writes_downstream_registers() -> None:
|
|||||||
system_payload = SystemConfigIn(time_sync="2026-05-25 12:34:56", brightness=80, screen_saver=60)
|
system_payload = SystemConfigIn(time_sync="2026-05-25 12:34:56", brightness=80, screen_saver=60)
|
||||||
switch_payload = SwitchControlIn(ch=3, action=1)
|
switch_payload = SwitchControlIn(ch=3, action=1)
|
||||||
|
|
||||||
|
read_channel = client.read_channel_config()
|
||||||
|
read_line_alarm = client.read_line_alarm_setting(2)
|
||||||
|
read_ai_alarm = client.read_ai_alarm_setting()
|
||||||
|
read_system = client.read_system_config()
|
||||||
|
read_time_sync = client.read_time_sync_config()
|
||||||
device_result = client.send_device_config(device_payload)
|
device_result = client.send_device_config(device_payload)
|
||||||
channel_result = client.send_channel_config(channel_payload)
|
channel_result = client.send_channel_config(channel_payload)
|
||||||
line_result = client.send_line_alarm_setting(line_alarm_payload)
|
line_result = client.send_line_alarm_setting(line_alarm_payload)
|
||||||
ai_alarm_result = client.send_ai_alarm_setting(ai_alarm_payload)
|
ai_alarm_result = client.send_ai_alarm_setting(ai_alarm_payload)
|
||||||
system_result = client.send_system_config(system_payload)
|
system_result = client.send_system_config(system_payload)
|
||||||
|
time_sync_result = client.send_time_sync_config(TimeSyncConfigIn(time_sync="2026-05-25 13:45:00"))
|
||||||
control_result = client.send_switch_control(switch_payload)
|
control_result = client.send_switch_control(switch_payload)
|
||||||
|
|
||||||
register_map = {address: values for address, values in transport.written_registers}
|
register_map = {address: values for address, values in transport.written_registers}
|
||||||
|
|
||||||
assert UART_CONFIG_START_REGISTER in register_map
|
assert UART_CONFIG_START_REGISTER in register_map
|
||||||
assert len(register_map[UART_CONFIG_START_REGISTER]) == UART_CONFIG_REGISTER_COUNT
|
assert len(register_map[UART_CONFIG_START_REGISTER]) == UART_CONFIG_REGISTER_COUNT
|
||||||
|
assert read_channel["ai_channel"][0]["singal_type"] == "4-20mA"
|
||||||
|
assert read_channel["ao_channel"][0]["type"] == "P"
|
||||||
|
assert read_line_alarm["line_no"] == 2
|
||||||
|
assert read_line_alarm["transformer_change"][0]["value"] == 10.0
|
||||||
|
assert read_line_alarm["transformer_change"][1]["value"] == 20.0
|
||||||
|
assert read_ai_alarm[0]["channel_no"] == 1
|
||||||
|
assert read_ai_alarm[0]["delay"] == 3
|
||||||
|
assert read_system["time_sync"] == "2026-05-25 12:34:56"
|
||||||
|
assert read_time_sync["time_sync"] == "2026-05-25 12:34:56"
|
||||||
|
|
||||||
assert AI_CHANNEL_START_REGISTER in register_map
|
assert AI_CHANNEL_START_REGISTER in register_map
|
||||||
assert len(register_map[AI_CHANNEL_START_REGISTER]) == 30
|
assert len(register_map[AI_CHANNEL_START_REGISTER]) == 30
|
||||||
assert AO_CHANNEL_START_REGISTER in register_map
|
assert AO_CHANNEL_START_REGISTER in register_map
|
||||||
assert len(register_map[AO_CHANNEL_START_REGISTER]) == 30
|
assert len(register_map[AO_CHANNEL_START_REGISTER]) == 30
|
||||||
assert LINE_ALARM_START_REGISTER + LINE_ALARM_REGISTERS_PER_LINE in register_map
|
assert LINE_ALARM_START_REGISTER + LINE_ALARM_REGISTERS_PER_LINE in register_map
|
||||||
assert len(register_map[LINE_ALARM_START_REGISTER + LINE_ALARM_REGISTERS_PER_LINE]) == 14
|
assert len(register_map[LINE_ALARM_START_REGISTER + LINE_ALARM_REGISTERS_PER_LINE]) == 16
|
||||||
assert AI_ALARM_START_REGISTER in register_map
|
assert AI_ALARM_START_REGISTER in register_map
|
||||||
assert len(register_map[AI_ALARM_START_REGISTER]) == 30
|
assert len(register_map[AI_ALARM_START_REGISTER]) == 30
|
||||||
assert TIME_SYNC_START_REGISTER in register_map
|
assert TIME_SYNC_START_REGISTER in register_map
|
||||||
@ -223,4 +289,7 @@ def test_modbus_client_writes_downstream_registers() -> None:
|
|||||||
assert line_result["line_no"] == 2
|
assert line_result["line_no"] == 2
|
||||||
assert ai_alarm_result["items"] == 1
|
assert ai_alarm_result["items"] == 1
|
||||||
assert system_result["send_status"] == "成功"
|
assert system_result["send_status"] == "成功"
|
||||||
|
assert "brightness" in system_result["ignored_fields"]
|
||||||
|
assert time_sync_result["target"] == "time_sync"
|
||||||
|
assert time_sync_result["time_sync"] == "2026-05-25 13:45:00"
|
||||||
assert "执行成功" in control_result["control_status"]
|
assert "执行成功" in control_result["control_status"]
|
||||||
|
|||||||
BIN
document/~$界面modbus协议寄存器定义v1.1.doc
Normal file
BIN
document/~$界面modbus协议寄存器定义v1.1.doc
Normal file
Binary file not shown.
@ -1,212 +1,420 @@
|
|||||||
# Modbus 协议寄存器定义文档
|
# Modbus 协议寄存器定义文档
|
||||||
|
|
||||||
## 一、协议基本配置
|
## 一、协议概览
|
||||||
|
|
||||||
- **协议类型**:Modbus RTU
|
| 项目 | 配置 |
|
||||||
- **从机地址**:1
|
| ---- | ---- |
|
||||||
- **串口配置**:
|
| 协议类型 | Modbus RTU |
|
||||||
- 波特率:115200
|
| 从机地址 | 1 |
|
||||||
- 数据位:8
|
| 波特率 | 115200 |
|
||||||
- 校验位:无
|
| 数据位 | 8 |
|
||||||
- 停止位:1
|
| 校验位 | 无 |
|
||||||
|
| 停止位 | 1 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 二、读数据(功能码:04)
|
## 二、寄存器总览
|
||||||
|
|
||||||
### 1. 实时显示数据
|
### 2.1 读数据区
|
||||||
|
|
||||||
- **寄存器类型**:输入寄存器
|
| 功能 | 功能码 | 寄存器类型 | 起始地址 | 长度/范围 | 说明 |
|
||||||
- **功能码**:0x04
|
| ---- | ------ | ---------- | -------- | --------- | ---- |
|
||||||
- **数据格式**:单精度浮点数(32位,占2个寄存器)
|
| 实时显示数据-线路1 | `0x04` | 输入寄存器 | `0x0000` | `0x0000 ~ 0x0032` | 单精度浮点数,32bit,2寄存器/点 |
|
||||||
|
| 实时显示数据-线路2 | `0x04` | 输入寄存器 | `0x0034` | `0x0034 ~ 0x0066` | 参数顺序同线路1 |
|
||||||
|
| 实时显示数据-线路3 | `0x04` | 输入寄存器 | `0x0068` | `0x0068 ~ 0x009A` | 参数顺序同线路1 |
|
||||||
|
| 实时显示数据-线路4 | `0x04` | 输入寄存器 | `0x009C` | `0x009C ~ 0x00CE` | 参数顺序同线路1 |
|
||||||
|
| AI输入 | `0x04` | 输入寄存器 | `0x00D0` | `0x00D0 ~ 0x00DE` | 8路 AI,单精度浮点数 |
|
||||||
|
| B码对时 | `0x04` | 输入寄存器 | `0x00E0` | `0x00E0 ~ 0x00E2` | 秒/分、时/日、月/年 |
|
||||||
|
| 开入状态 | `0x04` | 输入寄存器 | `0x00E3` | 1寄存器 | 16路,每 bit 一路 |
|
||||||
|
| 开出状态 | `0x04` | 输入寄存器 | `0x00E4` | 1寄存器 | 16路,每 bit 一路 |
|
||||||
|
| 事件总寄存器 | `0x04` | 输入寄存器 | `0x00E5` | 1寄存器 | 内容为 `n` 时表示有 `n` 个事件待读取 |
|
||||||
|
| 读事件内容 | `0x04` | 输入寄存器 | `0x0100` | `5 * n` | 当 `0x00E5 != 0` 时读取 |
|
||||||
|
|
||||||
#### 线路 #1
|
### 2.2 读/写参数区
|
||||||
- **寄存器地址范围**:0x0000 ~ 0x0032
|
|
||||||
|
| 功能 | 功能码 | 寄存器类型 | 起始地址 | 寄存器长度 | 说明 |
|
||||||
|
| ---- | ------ | ---------- | -------- | ---------- | ---- |
|
||||||
|
| 时间设置 | `0x03 / 0x10` | 保持寄存器 | `0x0000` | 2 | 时间戳,32bit |
|
||||||
|
| 串口设置 | `0x03 / 0x10` | 保持寄存器 | `0x0004` | 16 | 4路 × 8字节 |
|
||||||
|
| 线路定值设置 | `0x03 / 0x10` | 保持寄存器 | `0x0014` | 64 | 4路 × (7类定值 + 2个变比参数) |
|
||||||
|
| AI通道设置 | `0x03 / 0x10` | 保持寄存器 | `0x0054` | 30 | 12路整体下发 |
|
||||||
|
| AO通道设置 | `0x03 / 0x10` | 保持寄存器 | `0x0072` | 30 | 12路整体下发 |
|
||||||
|
| AI报警设置 | `0x03 / 0x10` | 保持寄存器 | `0x0090` | 30 | 12路整体下发 |
|
||||||
|
| 读事件成功回复 | `0x03` | 保持寄存器 | `0x0100` | 1 | 内容为本次成功读取的事件个数 `n` |
|
||||||
|
|
||||||
|
### 2.3 线圈区
|
||||||
|
|
||||||
|
| 功能 | 功能码 | 寄存器类型 | 起始地址 | 长度 | 说明 |
|
||||||
|
| ---- | ------ | ---------- | -------- | ---- | ---- |
|
||||||
|
| 写开出(批量) | `0x0F` | 线圈 | `0x0000` | 12 | 写多个线圈,控制12路开出 |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 三、实时数据寄存器表
|
||||||
|
|
||||||
|
### 3.1 线路1实时量
|
||||||
|
|
||||||
| 寄存器地址 | 参数名 | 寄存器地址 | 参数名 | 寄存器地址 | 参数名 |
|
| 寄存器地址 | 参数名 | 寄存器地址 | 参数名 | 寄存器地址 | 参数名 |
|
||||||
| ---------- | ------ | ---------- | ------ | ---------- | ------ |
|
| ---------- | ------ | ---------- | ------ | ---------- | ------ |
|
||||||
| 0x0000 | Ua | 0x0012 | Pt | 0x0024 | PFa |
|
| `0x0000` | Ua | `0x0012` | Pt | `0x0024` | PFa |
|
||||||
| 0x0002 | Ub | 0x0014 | Qa | 0x0026 | PFb |
|
| `0x0002` | Ub | `0x0014` | Qa | `0x0026` | PFb |
|
||||||
| 0x0004 | Uc | 0x0016 | Qb | 0x0028 | PFc |
|
| `0x0004` | Uc | `0x0016` | Qb | `0x0028` | PFc |
|
||||||
| 0x0006 | Ia | 0x0018 | Qc | 0x002A | PFt |
|
| `0x0006` | Ia | `0x0018` | Qc | `0x002A` | PFt |
|
||||||
| 0x0008 | Ib | 0x001A | Qt | 0x002C | Uab |
|
| `0x0008` | Ib | `0x001A` | Qt | `0x002C` | Uab |
|
||||||
| 0x000A | Ic | 0x001C | Sa | 0x002E | Ubc |
|
| `0x000A` | Ic | `0x001C` | Sa | `0x002E` | Ubc |
|
||||||
| 0x000C | Pa | 0x001E | Sb | 0x0030 | Uca |
|
| `0x000C` | Pa | `0x001E` | Sb | `0x0030` | Uca |
|
||||||
| 0x000E | Pb | 0x0020 | Sc | 0x0032 | Frq |
|
| `0x000E` | Pb | `0x0020` | Sc | `0x0032` | Frq |
|
||||||
| 0x0010 | Pc | 0x0022 | St | - | - |
|
| `0x0010` | Pc | `0x0022` | St | - | - |
|
||||||
|
|
||||||
#### 线路 #2
|
### 3.2 线路2~线路4地址范围
|
||||||
- **起始地址**:0x0034
|
|
||||||
- **结束地址**:0x0066
|
|
||||||
- **参数顺序**:同线路 #1
|
|
||||||
|
|
||||||
#### 线路 #3
|
| 线路 | 地址范围 | 说明 |
|
||||||
- **起始地址**:0x0068
|
| ---- | -------- | ---- |
|
||||||
- **结束地址**:0x009A
|
| 线路2 | `0x0034 ~ 0x0066` | 参数顺序同线路1 |
|
||||||
- **参数顺序**:同线路 #1
|
| 线路3 | `0x0068 ~ 0x009A` | 参数顺序同线路1 |
|
||||||
|
| 线路4 | `0x009C ~ 0x00CE` | 参数顺序同线路1 |
|
||||||
|
|
||||||
#### 线路 #4
|
### 3.3 AI输入寄存器表
|
||||||
- **起始地址**:0x009C
|
|
||||||
- **结束地址**:0x00CE
|
|
||||||
- **参数顺序**:同线路 #1
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. 模拟量输入(AI)
|
|
||||||
|
|
||||||
- **寄存器类型**:输入寄存器
|
|
||||||
- **功能码**:0x04
|
|
||||||
- **数据格式**:单精度浮点数(32位)
|
|
||||||
|
|
||||||
| 寄存器地址 | 输入编号 |
|
| 寄存器地址 | 输入编号 |
|
||||||
| ---------- | -------- |
|
| ---------- | -------- |
|
||||||
| 0x00D0 | Ai1 |
|
| `0x00D0` | Ai1 |
|
||||||
| 0x00D2 | Ai2 |
|
| `0x00D2` | Ai2 |
|
||||||
| 0x00D4 | Ai3 |
|
| `0x00D4` | Ai3 |
|
||||||
| 0x00D6 | Ai4 |
|
| `0x00D6` | Ai4 |
|
||||||
| 0x00D8 | Ai5 |
|
| `0x00D8` | Ai5 |
|
||||||
| 0x00DA | Ai6 |
|
| `0x00DA` | Ai6 |
|
||||||
| 0x00DC | Ai7 |
|
| `0x00DC` | Ai7 |
|
||||||
| 0x00DE | Ai8 |
|
| `0x00DE` | Ai8 |
|
||||||
|
|
||||||
|
### 3.4 B码对时寄存器表
|
||||||
|
|
||||||
|
| 寄存器地址 | 位分配 | 内容 |
|
||||||
|
| ---------- | ------ | ---- |
|
||||||
|
| `0x00E0` | `[15:8] = 秒`,`[7:0] = 分` | 秒/分 |
|
||||||
|
| `0x00E1` | `[15:8] = 时`,`[7:0] = 日` | 时/日 |
|
||||||
|
| `0x00E2` | `[15:8] = 月`,`[7:0] = 年` | 月/年 |
|
||||||
|
|
||||||
|
### 3.5 状态寄存器表
|
||||||
|
|
||||||
|
| 寄存器地址 | 含义 | 说明 |
|
||||||
|
| ---------- | ---- | ---- |
|
||||||
|
| `0x00E3` | 开入状态 | 16路输入,每 bit 一路 |
|
||||||
|
| `0x00E4` | 开出状态 | 16路输出,每 bit 一路 |
|
||||||
|
| `0x00E5` | 事件总寄存器 | 值为 `n` 表示有 `n` 个事件待读取 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3. B码对时
|
## 四、事件寄存器定义
|
||||||
|
|
||||||
- **寄存器类型**:输入寄存器
|
### 4.1 读事件内容
|
||||||
- **功能码**:0x04
|
|
||||||
- **数据格式**:无符号16位整型
|
|
||||||
|
|
||||||
| 寄存器地址 | 位分配 | 内容 |
|
| 项目 | 定义 |
|
||||||
| ---------- | ------------------------- | ----- |
|
| ---- | ---- |
|
||||||
| 0x00E0 | [15:8] = 秒<br>[7:0] = 分 | 秒/分 |
|
| 触发条件 | 事件总寄存器 `0x00E5 != 0` |
|
||||||
| 0x00E1 | [15:8] = 时<br>[7:0] = 日 | 时/日 |
|
| 功能码 | `0x04` |
|
||||||
| 0x00E2 | [15:8] = 月<br>[7:0] = 年 | 月/年 |
|
| 寄存器类型 | 输入寄存器 |
|
||||||
|
| 起始地址 | `0x0100` |
|
||||||
|
| 读取长度 | `5 * n` |
|
||||||
|
|
||||||
|
| 数据名 | 数据寄存器 | 格式 | 说明 |
|
||||||
|
| ------ | ---------- | ---- | ---- |
|
||||||
|
| 时间戳 | `0x0100 ~ 0x0101` | 32位无符号 | 事件发生时间戳 |
|
||||||
|
| 毫秒计时 | `0x0102` | 16位无符号 | 事件发生毫秒值 |
|
||||||
|
| 线路号 + 事件类型 | `0x0103` | 16位无符号 | 高4位为线路号,低4位为事件类型 |
|
||||||
|
| 事件代码 | `0x0103` 低8位扩展语义 | 8位无符号 | 按事件类型解释具体代码 |
|
||||||
|
| 动作值 | `0x0104` | 16位无符号 | 监测值或动作状态值 |
|
||||||
|
|
||||||
|
### 4.2 字段取值说明
|
||||||
|
|
||||||
|
#### 线路号
|
||||||
|
|
||||||
|
| 值 | 含义 |
|
||||||
|
| -- | ---- |
|
||||||
|
| `0` | 装置 |
|
||||||
|
| `1` | 线路1 |
|
||||||
|
| `2` | 线路2 |
|
||||||
|
| `3` | 线路3 |
|
||||||
|
| `4` | 线路4 |
|
||||||
|
|
||||||
|
#### 事件类型
|
||||||
|
|
||||||
|
| 值 | 含义 |
|
||||||
|
| -- | ---- |
|
||||||
|
| `1` | 保护类 |
|
||||||
|
| `2` | 动作类 |
|
||||||
|
| `3` | 报警类 |
|
||||||
|
| `4` | 操作类 |
|
||||||
|
| `5` | AI越限类 |
|
||||||
|
|
||||||
|
#### 事件代码
|
||||||
|
|
||||||
|
| 类别 | 代码定义 |
|
||||||
|
| ---- | -------- |
|
||||||
|
| 保护类 | `1 = PT断线`,`2 = CT断线`,其余依次编号 |
|
||||||
|
| 报警类 | `1 = 过压`,`2 = 过流`,`3 = 频率越限` 等 |
|
||||||
|
| 动作类 | 开入序号,表示第 `n` 路发生状态变化 |
|
||||||
|
| AI越限类 | AI通道号,表示第 `n` 通道发生越限 |
|
||||||
|
| 操作类 | 由主站界面操作直接生成记录 |
|
||||||
|
|
||||||
|
#### 动作值
|
||||||
|
|
||||||
|
| 类别 | 含义 |
|
||||||
|
| ---- | ---- |
|
||||||
|
| 保护类 / 报警类 | 越限时的监测值 |
|
||||||
|
| 动作类 | 开入/开出状态,`0 -> 分`,非 `0 -> 合` |
|
||||||
|
|
||||||
|
### 4.3 读事件成功回复
|
||||||
|
|
||||||
|
| 项目 | 定义 |
|
||||||
|
| ---- | ---- |
|
||||||
|
| 功能码 | `0x03` |
|
||||||
|
| 寄存器类型 | 保持寄存器 |
|
||||||
|
| 起始地址 | `0x0100` |
|
||||||
|
| 数据长度 | 1 |
|
||||||
|
| 内容 | 本次成功读取的事件个数 `n` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 4. 数字量输入(开入)
|
## 五、参数区寄存器说明
|
||||||
|
|
||||||
- **寄存器类型**:输入寄存器
|
### 5.1 参数页读写规则
|
||||||
- **功能码**:0x04
|
|
||||||
- **寄存器地址**:0x00E3
|
- 参数页在开机及切换到参数类页面时,应先发送 `03H` 读取参数。
|
||||||
- **说明**:共16路,每个bit位表示一路开入量
|
- 参数发生修改后,应发送 `10H` 下发更新。
|
||||||
|
- 主站应在参数修改成功后生成对应“参数修改”事件,并实时显示、存盘。
|
||||||
|
- 对时功能除可在系统设置页统一下发外,也可通过上位机独立“对时设置”接口单独触发,底层仍写时间设置寄存器 `0x0000`。
|
||||||
|
|
||||||
|
### 5.2 参数区表格
|
||||||
|
|
||||||
|
| 参数类别 | 功能码 | 寄存器类型 | 起始地址 | 长度 | 说明 |
|
||||||
|
| -------- | ------ | ---------- | -------- | ---- | ---- |
|
||||||
|
| 时间设置 | `0x03 / 0x10` | 保持寄存器 | `0x0000` | 2 | 时间戳,32bit |
|
||||||
|
| 串口设置 | `0x03 / 0x10` | 保持寄存器 | `0x0004` | 16 | 4路 × 8字节 |
|
||||||
|
| 线路定值设置 | `0x03 / 0x10` | 保持寄存器 | `0x0014` | 64 | 含7类定值与2个变比参数 |
|
||||||
|
| AI通道设置 | `0x03 / 0x10` | 保持寄存器 | `0x0054` | 30 | 12路整体下发 |
|
||||||
|
| AO通道设置 | `0x03 / 0x10` | 保持寄存器 | `0x0072` | 30 | 12路整体下发 |
|
||||||
|
| AI报警设置 | `0x03 / 0x10` | 保持寄存器 | `0x0090` | 30 | 12路整体下发 |
|
||||||
|
| 开出状态字写入 | `0x10` | 保持寄存器 | `0x00A6` | 1 | 16位状态字,每 bit 一路开出 |
|
||||||
|
|
||||||
|
### 5.2.1 时间设置 / 独立对时说明
|
||||||
|
|
||||||
|
| 项目 | 定义 |
|
||||||
|
| ---- | ---- |
|
||||||
|
| 寄存器类型 | 保持寄存器 |
|
||||||
|
| 功能码 | `0x03 / 0x10` |
|
||||||
|
| 起始地址 | `0x0000` |
|
||||||
|
| 寄存器长度 | 2 |
|
||||||
|
| 数据格式 | 32 位时间戳,低 16 位在前,高 16 位在后 |
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 主站可在系统设置页统一读取/下发该寄存器。
|
||||||
|
- 主站也可通过独立“对时设置”功能,单独向该寄存器写入目标时间。
|
||||||
|
- 上位机后端可额外开放独立对时接口,例如:
|
||||||
|
- `GET /api/config/time-sync`
|
||||||
|
- `POST /api/config/time-sync`
|
||||||
|
- 以上接口属于上位机软件封装,底层协议仍然只对应本节 `0x0000` 时间设置寄存器。
|
||||||
|
|
||||||
|
### 5.3 通讯设置
|
||||||
|
|
||||||
|
#### 5.3.1 基本定义
|
||||||
|
|
||||||
|
| 项目 | 定义 |
|
||||||
|
| ---- | ---- |
|
||||||
|
| 寄存器类型 | 保持寄存器 |
|
||||||
|
| 功能码 | `0x03 / 0x10` |
|
||||||
|
| 起始地址 | `0x0004` |
|
||||||
|
| 寄存器长度 | `16` |
|
||||||
|
| 长度说明 | `4(路) × 4(8byte数据/2) = 16` |
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 对应界面中的“串口设置”区域。
|
||||||
|
- 共支持 `4` 路串口参数读写。
|
||||||
|
- 每一路串口参数占用 `8` 字节,即 `4` 个保持寄存器。
|
||||||
|
- 主站进入参数页面时应先使用 `03H` 回读当前串口参数,界面修改后再使用 `10H` 整组下发。
|
||||||
|
|
||||||
|
#### 5.3.2 单路串口参数组成
|
||||||
|
|
||||||
|
单路串口参数结构对应 `com_param_t`,总长度 `8` 字节:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 字节数 | 说明 |
|
||||||
|
| ---- | ---- | ------ | ---- |
|
||||||
|
| `baud_rate` | `uint32_t` | 4 | 波特率 |
|
||||||
|
| `parity` | `uint8_t` | 1 | 校验位 |
|
||||||
|
| `data` | `uint8_t` | 1 | 数据位 |
|
||||||
|
| `stop` | `uint8_t` | 1 | 停止位 |
|
||||||
|
| `protocol` | `uint8_t` | 1 | 通讯协议 |
|
||||||
|
|
||||||
|
#### 5.3.3 寄存器分配
|
||||||
|
|
||||||
|
| 串口 | 寄存器范围 | 长度 | 说明 |
|
||||||
|
| ---- | ---------- | ---- | ---- |
|
||||||
|
| 串口1 | `0x0004 ~ 0x0007` | 4 | 对应界面中的一组串口参数 |
|
||||||
|
| 串口2 | `0x0008 ~ 0x000B` | 4 | 对应界面中的一组串口参数 |
|
||||||
|
| 串口3 | `0x000C ~ 0x000F` | 4 | 预留/扩展串口参数 |
|
||||||
|
| 串口4 | `0x0010 ~ 0x0013` | 4 | 预留/扩展串口参数 |
|
||||||
|
|
||||||
|
#### 5.3.4 界面字段对应关系
|
||||||
|
|
||||||
|
对应界面中的典型字段如下:
|
||||||
|
|
||||||
|
| 界面字段 | 协议字段 |
|
||||||
|
| -------- | -------- |
|
||||||
|
| 串口号 | 由寄存器块位置区分,不单独占用字段 |
|
||||||
|
| 波特率 | `baud_rate` |
|
||||||
|
| 校验位 | `parity` |
|
||||||
|
| 数据位 | `data` |
|
||||||
|
| 停止位 | `stop` |
|
||||||
|
| 通讯协议 | `protocol` |
|
||||||
|
|
||||||
|
### 5.4 常规配置 / 网卡设置
|
||||||
|
|
||||||
|
#### 5.4.1 协议定义说明
|
||||||
|
|
||||||
|
| 项目 | 定义 |
|
||||||
|
| ---- | ---- |
|
||||||
|
| 配置项 | 网卡号、IP地址、子网掩码、默认网关、通讯协议 |
|
||||||
|
| 当前协议状态 | 本版 Modbus 协议**未定义对应寄存器** |
|
||||||
|
| Modbus 读写支持 | 不支持 |
|
||||||
|
| 处理方式 | 由上位机或系统本地配置逻辑单独处理,不纳入当前 Modbus 参数区 |
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 对应界面中的“常规配置 / 网卡设置”区域。
|
||||||
|
- 当前文档已定义“串口设置”的 Modbus 参数区,但**未定义网卡设置的寄存器地址、长度和编码方式**。
|
||||||
|
- 因此本版协议下,网卡参数**不能**按 `03H / 10H` 通过 Modbus 回读或下发。
|
||||||
|
- 若后续需要将网卡参数纳入 Modbus 管理,建议在协议后续版本中补充:
|
||||||
|
- 寄存器类型
|
||||||
|
- 功能码
|
||||||
|
- 起始地址
|
||||||
|
- 寄存器长度
|
||||||
|
- IP / 掩码 / 网关 / 协议字段编码规则
|
||||||
|
|
||||||
|
#### 5.4.2 界面字段范围
|
||||||
|
|
||||||
|
对应界面中的典型字段如下:
|
||||||
|
|
||||||
|
| 界面字段 | 当前协议状态 |
|
||||||
|
| -------- | ------------ |
|
||||||
|
| 网卡号 | 未定义寄存器 |
|
||||||
|
| IP地址 | 未定义寄存器 |
|
||||||
|
| 子网掩码 | 未定义寄存器 |
|
||||||
|
| 默认网关 | 未定义寄存器 |
|
||||||
|
| 通讯协议 | 未定义寄存器 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 5. 事件总标记
|
## 六、开出控制说明
|
||||||
|
|
||||||
- **寄存器类型**:输入寄存器
|
### 6.1 写保持寄存器方式
|
||||||
- **功能码**:0x04
|
|
||||||
- **寄存器地址**:0x00E4
|
| 项目 | 定义 |
|
||||||
- **说明**:当该位被置位时,表示有新事件,需要读取事件内容
|
| ---- | ---- |
|
||||||
|
| 功能码 | `0x10` |
|
||||||
|
| 寄存器类型 | 保持寄存器 |
|
||||||
|
| 起始地址 | `0x00A6` |
|
||||||
|
| 长度 | 1 |
|
||||||
|
| 说明 | 16位状态字,每 bit 表示一路开出 |
|
||||||
|
|
||||||
|
### 6.2 写线圈方式
|
||||||
|
|
||||||
|
| 项目 | 定义 |
|
||||||
|
| ---- | ---- |
|
||||||
|
| 功能码 | `0x0F` |
|
||||||
|
| 寄存器类型 | 线圈 |
|
||||||
|
| 起始地址 | `0x0000` |
|
||||||
|
| 长度 | 12 |
|
||||||
|
| 说明 | 写多个线圈,控制12路开出 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 三、读事件(功能码:03)
|
## 七、通讯时序说明
|
||||||
|
|
||||||
- **触发条件**:事件总标记(0x00E4)被置位
|
### 7.1 开机初始化时序
|
||||||
- **寄存器类型**:保持寄存器
|
|
||||||
- **功能码**:0x03
|
|
||||||
- **寄存器地址范围**:0x0000 ~ 0x0003
|
|
||||||
|
|
||||||
| 寄存器地址 | 数据名 | 格式 | 说明 |
|
1. 主站开机后,先通过 `03H` 读取参数区。
|
||||||
| ---------- | ---------------- | ---------- | ---------------------------- |
|
2. 若参数读取失败,主站生成报警事件,可归类为“开机自检(通讯链路)”异常。
|
||||||
| 0x0000 | 时间戳(低16位) | 32位无符号 | 占用0x0000~0x0001 |
|
3. 开机自检结束后,进入周期轮询阶段。
|
||||||
| 0x0001 | 时间戳(高16位) | | |
|
|
||||||
| 0x0002 | 毫秒计时 | 16位无符号 | |
|
|
||||||
| 0x0003 | 线路号+事件类型 | 16位无符号 | 高字节线路号,低字节事件类型 |
|
|
||||||
|
|
||||||
---
|
### 7.2 周期轮询时序
|
||||||
|
|
||||||
## 四、读输入数字量(开入)(功能码:02)
|
1. 主站周期发送 `04H`,读取实时输入寄存器。
|
||||||
|
2. 收到从站 `04H` 应答后,更新实时数据、状态数据。
|
||||||
|
3. 随后读取事件总寄存器 `0x00E5`。
|
||||||
|
4. 若 `0x00E5 = 0`,继续下一轮周期轮询。
|
||||||
|
5. 若 `0x00E5 = n` 且 `n > 0`,立即进入读事件流程。
|
||||||
|
|
||||||
- **寄存器类型**:离散输入
|
补充说明:
|
||||||
- **功能码**:0x02
|
|
||||||
|
|
||||||
| 寄存器地址 | 说明 |
|
- 若上位机需要在页面上动态显示设备当前时间,可额外按固定周期读取时间设置寄存器 `0x0000`。
|
||||||
| ---------- | --------------------- |
|
- 推荐上位机软件每 `1` 秒轮询一次,并通过独立时间推送通道发送给前端页面。
|
||||||
| 0x0000 | 16路开入量,每bit一路 |
|
- 推荐的软件推送通道示例:`/ws/time-sync`
|
||||||
| 0x0001 | 保留(16路) |
|
|
||||||
|
|
||||||
---
|
### 7.3 读事件时序
|
||||||
|
|
||||||
## 五、写数据(功能码:10,写多个保持寄存器)
|
1. 主站发送“读事件”指令:
|
||||||
|
- 功能码:`04H`
|
||||||
|
- 输入寄存器地址:`0x0100`
|
||||||
|
- 读取长度:`5 * n`
|
||||||
|
2. 主站收到正确应答后,解析本批事件内容。
|
||||||
|
3. 主站发送“读事件成功回复”:
|
||||||
|
- 功能码:`03H`
|
||||||
|
- 保持寄存器地址:`0x0100`
|
||||||
|
- 长度:`1`
|
||||||
|
- 内容:本次成功读取的事件个数 `n`
|
||||||
|
4. 从站收到回复后,应将事件总寄存器 `0x00E5` 中的事件个数扣减 `n`。
|
||||||
|
5. 若扣减后 `0x00E5` 仍非 0,主站应继续重复读事件流程,直到 `0x00E5 = 0`。
|
||||||
|
|
||||||
### 1. 时间设置
|
### 7.4 事件生成与保存
|
||||||
|
|
||||||
- **寄存器类型**:保持寄存器
|
- 从站负责生成并暂存:
|
||||||
- **功能码**:0x10
|
- 报警类事件
|
||||||
- **起始地址**:0x0000
|
- 保护类事件
|
||||||
- **寄存器长度**:2
|
- 动作类事件
|
||||||
- **说明**:写入时间戳(32位)
|
- 主站负责生成:
|
||||||
|
- 操作类事件
|
||||||
|
|
||||||
---
|
主站侧操作类事件至少包括:
|
||||||
|
|
||||||
### 2. 串口设置
|
| 代码 | 含义 |
|
||||||
|
| ---- | ---- |
|
||||||
|
| `1` | 控制分/合 |
|
||||||
|
| `2` | 读定值失败 |
|
||||||
|
| `3` | 线路定值修改 |
|
||||||
|
| `4` | AI报警参数修改 |
|
||||||
|
| `5` | AI通道参数修改 |
|
||||||
|
| `6` | AO通道参数修改 |
|
||||||
|
| `7` | 通讯参数修改 |
|
||||||
|
| `8` | 密码修改 |
|
||||||
|
|
||||||
- **寄存器类型**:保持寄存器
|
统一要求:
|
||||||
- **功能码**:0x10
|
|
||||||
- **起始地址**:0x0004
|
|
||||||
- **寄存器长度**:16
|
|
||||||
- **说明**:4路 × 4(8字节数据/2) = 16个寄存器
|
|
||||||
|
|
||||||
---
|
- 从站生成的事件与主站生成的事件,均保存在主站端。
|
||||||
|
- 主站应形成历史事件记录文件。
|
||||||
|
- 历史事件应支持按分类、时间段、线路号等条件检索。
|
||||||
|
|
||||||
### 3. 线路定值设置
|
### 7.5 时间推送建议
|
||||||
|
|
||||||
- **寄存器类型**:保持寄存器
|
为便于上位机页面显示设备当前时间,建议增加独立时间推送链路:
|
||||||
- **功能码**:0x10
|
|
||||||
- **起始地址**:0x0014
|
|
||||||
- **寄存器长度**:56
|
|
||||||
- **说明**:4路 × 7类别 × 2(4字节数据/2) = 56个寄存器
|
|
||||||
|
|
||||||
---
|
1. 后台服务每 `1` 秒读取一次时间设置寄存器 `0x0000`。
|
||||||
|
2. 将回读结果转换为标准时间字符串。
|
||||||
|
3. 通过独立 WebSocket 通道推送给页面。
|
||||||
|
|
||||||
### 4. AI通道设置
|
推荐推送报文格式:
|
||||||
|
|
||||||
- **寄存器类型**:保持寄存器
|
```json
|
||||||
- **功能码**:0x10
|
{
|
||||||
- **起始地址**:0x004C
|
"type": "time_sync",
|
||||||
- **寄存器长度**:30
|
"data": {
|
||||||
- **说明**:12路 × 5字节数据 / 2 = 30个寄存器(整体下发)
|
"time_sync": "2026-05-25 14:30:01"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
---
|
说明:
|
||||||
|
|
||||||
### 5. AO通道设置
|
- 该推送属于上位机软件实现建议,不新增下位机寄存器。
|
||||||
|
- 底层时间真值仍以 `0x0000` 时间设置寄存器为准。
|
||||||
- **寄存器类型**:保持寄存器
|
|
||||||
- **功能码**:0x10
|
|
||||||
- **起始地址**:0x006A
|
|
||||||
- **寄存器长度**:30
|
|
||||||
- **说明**:12路 × 5字节数据 / 2 = 30个寄存器(整体下发)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 6. AI报警设置
|
|
||||||
|
|
||||||
- **寄存器类型**:保持寄存器
|
|
||||||
- **功能码**:0x10
|
|
||||||
- **起始地址**:0x0088
|
|
||||||
- **寄存器长度**:30
|
|
||||||
- **说明**:12路 × 5字节数据 / 2 = 30个寄存器(整体下发)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 7. 开出(写继电器输出)
|
|
||||||
|
|
||||||
- **寄存器类型**:保持寄存器
|
|
||||||
- **功能码**:0x10
|
|
||||||
- **起始地址**:0x00A6
|
|
||||||
- **寄存器长度**:1
|
|
||||||
- **数据格式**:16位,每一位表示一路开出量
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 六、写开出(线圈)(功能码:05)
|
|
||||||
|
|
||||||
- **寄存器类型**:线圈
|
|
||||||
- **功能码**:0x05
|
|
||||||
- **起始地址**:0x0000
|
|
||||||
- **寄存器长度**:12
|
|
||||||
- **说明**:单路线圈写操作(12路分别操作)
|
|
||||||
|
|||||||
BIN
document/显示界面modbus协议寄存器定义v1.1.doc
Normal file
BIN
document/显示界面modbus协议寄存器定义v1.1.doc
Normal file
Binary file not shown.
160
document/显示界面modbus协议寄存器定义v1.1.extracted.txt
Normal file
160
document/显示界面modbus协议寄存器定义v1.1.extracted.txt
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
一、使用协议
|
||||||
|
1、 基于Modbus RTU协议
|
||||||
|
2、 从机地址:1
|
||||||
|
3、 串口配置:
|
||||||
|
(1) 波特率:115200
|
||||||
|
(2) 数据位:8
|
||||||
|
(3) 校验位:无
|
||||||
|
(4) 停止位:1
|
||||||
|
二、 读数据
|
||||||
|
1、实时显示数据:
|
||||||
|
(1) 寄存器类型:输入寄存器
|
||||||
|
(2) 功能码:04
|
||||||
|
(3) 数据格式:单精度浮点数(32bit)
|
||||||
|
(4) 线路#1:寄存器地址 0x0000 ~ 0x0032(如表1)
|
||||||
|
寄存器地址
寄存器名
寄存器地址
寄存器名
寄存器地址
寄存器名
0x0000
Ua
0x0012
Pt
0x0024
PFa
0x0002
Ub
0x0014
Qa
0x0026
PFb
0x0004
Uc
0x0016
Qb
0x0028
PFc
0x0006
Ia
0x0018
Qc
0x002a
PFt
0x0008
Ib
0x001a
Qt
0x002c
Uab
0x000a
Ic
0x001c
Sa
0x002e
Ubc
0x000c
Pa
0x001e
Sb
0x0030
Uca
0x000e
Pb
0x0020
Sc
0x0032
Frq
0x0010
Pc
0x0022
St
表1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
(5) 线路#2:寄存器地址 0x0034 ~ 0x0066 (寄存器名排列同表1)
|
||||||
|
(6) 线路#3:寄存器地址 0x0068 ~ 0x009a (寄存器名排列同表1)
|
||||||
|
(7) 线路#4:寄存器地址 0x009c ~ 0x00ce (寄存器名排列同表1)
|
||||||
|
|
||||||
|
2、模拟量输入(AI)
|
||||||
|
(1) 寄存器类型:输入寄存器
|
||||||
|
(2) 功能码:04
|
||||||
|
(3) 数据格式:单精度浮点数(32bit)
|
||||||
|
(4) 寄存器地址 0x00d0 ~ 0x00de
|
||||||
|
|
||||||
|
寄存器地址
0x00d0
0x00d2
0x00d4
0x00d6
0x00d8
0x00da
0x00dc
0x00de
输入编号
Ai1
Ai2
Ai3
Ai4
Ai5
Ai6
Ai7
Ai8
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
3、 B码对时
|
||||||
|
(1) 寄存器类型:输入寄存器
|
||||||
|
(2) 功能码:04
|
||||||
|
(3) 数据格式:无符号短整型(16bit)
|
||||||
|
(4) 寄存器地址 0x00e0 ~ 0x00e2
|
||||||
|
|
||||||
|
寄存器地址
0x00e0
0x00e1
0x00e2
变量地址分配
[15:8]秒 [7:0]分
[15:8]时 [7:0]日
[15:8]月 [7:0]年
|
||||||
|
4、 数字量输入 (开入)
|
||||||
|
(1) 寄存器类型:输入寄存器
|
||||||
|
(2) 功能码:04
|
||||||
|
(3) 寄存器地址 0x00e3 (16路)
|
||||||
|
(4) 每一bit位表示一路开入量
|
||||||
|
|
||||||
|
5、 开出量状态
|
||||||
|
(1) 寄存器类型:输入寄存器
|
||||||
|
(2) 功能码:04
|
||||||
|
(3) 起始寄存器地址:0x00e4(16路)
|
||||||
|
(4) 1每一位表示一路开出量当前状态
|
||||||
|
|
||||||
|
6、 事件总寄存器 (非0时,代表事件个数)
|
||||||
|
(1) 寄存器类型:输入寄存器
|
||||||
|
(2) 功能码:04
|
||||||
|
(3) 寄存器地址 0x00e5
|
||||||
|
|
||||||
|
三、 读事件
|
||||||
|
当主机轮询到事件总寄存器非0时,读事件寄存器,获取事件内容。
|
||||||
|
(1) 寄存器类型:输入寄存器
|
||||||
|
(2) 功能码:04
|
||||||
|
(3) 寄存器地址 0x0100
|
||||||
|
(4) 读取长度 5 *n (5 = 寄存器个数,n = 事件个数)
|
||||||
|
数据名
数据寄存器
时间戳 (32bit)
0x0100
毫秒计时(16bit)
0x0102
线路号+事件类型(8bit)
0x0103
高4位:
|
||||||
|
线路号(0 = 装置,1 = 线路1,2 =线路2, 3 = 线路3, 4 =线路 4)
|
||||||
|
低4位:
|
||||||
|
事件类型(1 = 保护类、2 = 动作类、3 = 报警类、4 = 操作类、5=AI越限类)
|
||||||
|
事件代码(8bit)
保护类:1 = pt 断线, 2 = CT断线,其它事件依次编号
|
||||||
|
报警类:1 = 过压,2 = 过流,3 =频率越限,4 =
|
||||||
|
动作类:开入序号(第n路发生了状态变化)
|
||||||
|
AI越限类:AI通道号(第n通道发生越限)
|
||||||
|
操作类:界面操作直接生成记录,显示并存盘即可
动作值(16bit)
0x0104
保护类、报警类:越限时的监测值
|
||||||
|
动作类:开入/开出的状态,0→分,非零→合;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
读事件成功回复(主站将本次读取到的事件个数写入到寄存器中,从站收到该报文表示主站读取事件成功):
|
||||||
|
(1) 寄存器类型:保持寄存器
|
||||||
|
(2) 功能码:03
|
||||||
|
(3) 寄存器地址 0x0100
|
||||||
|
(4) 长度:1
|
||||||
|
(5) 内容:事件个数
|
||||||
|
1000
|
||||||
|
四、 读输入数字量 (开入)
|
||||||
|
(1) 寄存器类型:离散输入
|
||||||
|
(2) 功能码:02
|
||||||
|
(3) 寄存器地址 0x0000 (16路)
|
||||||
|
(4) 保留寄存器地址:0x0001(16路)
|
||||||
|
五、 参数设置与读取
|
||||||
|
写参数功能码:10H
|
||||||
|
读参数功能码:03H
|
||||||
|
显示界面在开机和切换到参数类界面时通过功能码03向下读取参数。
|
||||||
|
1、 时间设置(时间戳)
|
||||||
|
(1) 寄存器类型:保持寄存器
|
||||||
|
(2) 功能码:03/10
|
||||||
|
(3) 寄存器地址:0x0000
|
||||||
|
(4) 寄存器长度:2
|
||||||
|
|
||||||
|
2、 通讯设置
|
||||||
|
(1) 寄存器类型:保持寄存器
|
||||||
|
(2) 功能码:03/10
|
||||||
|
(3) 起始地址:0x0004
|
||||||
|
(4) 寄存器长度:4(路) * 4(8byte数据/2) = 16
|
||||||
|
|
||||||
|
3、 线路定值设置
|
||||||
|
(1) 寄存器类型:保持寄存器
|
||||||
|
(2) 功能码:03/10
|
||||||
|
(3) 起始寄存器地址:0x0014
|
||||||
|
(4) 寄存器长度:4(路)*(7(类别) *2(4byte数据/2) + 2(变比)) = 64
|
||||||
|
|
||||||
|
4、 AI通道设置
|
||||||
|
(1) 寄存器类型:保持寄存器
|
||||||
|
(2) 功能码:03/10
|
||||||
|
(3) 起始寄存器地址:0x0054
|
||||||
|
(4) 寄存器长度:(12(路)*5byte数据)/2 = 30 (参数设计为12路整体下发,不用担心参数截断问题)
|
||||||
|
|
||||||
|
5、 AO通道设置
|
||||||
|
(1) 寄存器类型:保持寄存器
|
||||||
|
(2) 功能码:03/10
|
||||||
|
(3) 起始寄存器地址:0x0072
|
||||||
|
(4) 寄存器长度:(12(路)*5byte数据)/2 = 30 (参数设计为12路整体下发,不用担心参数截断问题)
|
||||||
|
|
||||||
|
6、 AI报警设置
|
||||||
|
(1) 寄存器类型:保持寄存器
|
||||||
|
(2) 功能码:03/10
|
||||||
|
(3) 起始寄存器地址:0x0090
|
||||||
|
(4) 寄存器长度:(12(路)*5byte数据)/2 = 30 (参数设计为12路整体下发,不用担心参数截断问题)
|
||||||
|
|
||||||
|
六、 写开出(线圈)
|
||||||
|
(1) 寄存器类型:线圈
|
||||||
|
(2) 功能码:0F(写多个线圈)
|
||||||
|
(3) 起始寄存器地址:0x0000
|
||||||
|
(4) 寄存器长度:12
|
||||||
|
|
||||||
|
七、 通讯过程补充说明
|
||||||
|
(1) 主站开机后读取参数发送03功能码,若读取失败生成报警事件。该过程可归为开机自检(通讯链路);
|
||||||
|
(2) 开机自检结束后,开始定期轮询输入寄存器(发送04功能码)。收到从站04应答帧,主站更新数据,并查询事件总寄存器(输入寄存器0x00e5),该寄存器非0(从站有事件发生)时,主站应该立即发送"读事件"指令,正确读取事件并成功回复后,立即再次发送04功能码读输入寄存器并查询事件总寄存器(输入寄存器0x00e5),若该寄存器仍非0,则立即再次发送"读事件"指令,直至该寄存器为0。
|
||||||
|
当事件总寄存器(输入寄存器0x00e5)为0时,定期轮询输入寄存器(发送04功能码)。
|
||||||
|
(3) 事件总寄存器,输入寄存器地址 0x00e5,该寄存器内容为n时(非0),表示从站有n个事件发生,主站应该立即发送"读事件"指令(功能码04,输入寄存器地址0x0100,读取长度 5 *n)。
|
||||||
|
主站正确收到"读事件"指令的应答帧后,发读事件成功回复(功能码03,保持寄存器地址0x0100,数据长度1,内容n)。
|
||||||
|
从站收到主站读事件成功回复帧,事件总寄存器(输入寄存器0x00e5)中事件个数-n。
|
||||||
|
(4) 关于事件生成与保存
|
||||||
|
* 事件类型:1 = 保护类、2 = 动作类、3 = 报警类、4 = 操作类
|
||||||
|
* 从站生成:"报警""保护""动作"类事件,并暂存;
|
||||||
|
* 主站生成:"操作"类事件,事件代码(1=控制分/合(动作值0→分、动作值非0→合)、2=读定值失败、3=线路定值修改、4=AI报警参数修改、5=AI通道参数修改、6=AO通道参数修改、7=通讯参数修改、8=密码修改);
|
||||||
|
* 事件保存:从站生成的事件、主站生成的事件统一保存在主站端,并形成历史事件记录文件,并可分类、按时间段、线路号等检索查看。
|
||||||
|
(5) 显示页面切换到参数页时,应当发送"读参数功能码03H",当参数发生更改时,发送"写参数功能码10H",并由主站生成"xx参数修改"事件,实时显示并存盘形成历史事件。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
1354
document/显示界面modbus协议寄存器定义v1.1.filtered.html
Normal file
1354
document/显示界面modbus协议寄存器定义v1.1.filtered.html
Normal file
File diff suppressed because it is too large
Load Diff
@ -31,7 +31,12 @@
|
|||||||
|
|
||||||
#### 2.2.1 参数配置管理功能
|
#### 2.2.1 参数配置管理功能
|
||||||
|
|
||||||
实现测控设备全维度参数配置,配置项支持**JSON文件本地存储**,关键参数通过接口下发至C语言嵌入式程序,具体配置项如下:
|
实现测控设备全维度参数配置。结合当前 Modbus 协议与后端实现,配置管理分为两类:
|
||||||
|
|
||||||
|
- **协议已定义寄存器的配置项**:以**下位机寄存器值为唯一真值**,页面首次加载时由后端通过 Modbus 回读显示;保存时后端只动态写入下位机,不再落本地 JSON 文件。
|
||||||
|
- **协议暂未定义寄存器的配置项**:接口先保留,继续使用本地 JSON 文件逻辑作为过渡,待后续协议补齐后再迁移到纯 Modbus 模式。
|
||||||
|
|
||||||
|
具体配置项如下:
|
||||||
|
|
||||||
1. **基础设备配置**
|
1. **基础设备配置**
|
||||||
- 设备软件版本信息展示与配置
|
- 设备软件版本信息展示与配置
|
||||||
@ -42,11 +47,11 @@
|
|||||||
- AO通道设置:模拟量输出配置
|
- AO通道设置:模拟量输出配置
|
||||||
- 定值设置:电压/电流/功率等超限限值、检测延时、报警输出配置
|
- 定值设置:电压/电流/功率等超限限值、检测延时、报警输出配置
|
||||||
3. **系统管理配置**
|
3. **系统管理配置**
|
||||||
- 对时设置:参数实时下发嵌入式程序
|
- 对时设置:按 Modbus 协议实时下发嵌入式程序
|
||||||
- 灯光设置:屏幕亮度、屏保时间,参数实时下发嵌入式程序
|
- 灯光设置:屏幕亮度、屏保时间字段当前仅保留接口兼容性,因协议暂未定义寄存器,本轮后端不通过 Modbus 下发
|
||||||
4. **配置回显要求**
|
4. **配置回显要求**
|
||||||
- 设备网络配置、通道配置、报警设置、系统设置页面在打开时需先读取已保存参数
|
- 通道配置、报警设置、系统设置页面在打开时,需先从下位机读取当前寄存器参数后显示
|
||||||
- 已保存配置从本地JSON文件读取,并通过HTTP查询接口返回给Web界面显示
|
- 设备/网络/密码等协议未定义寄存器的配置项,当前仍从本地 JSON 文件读取并通过 HTTP 接口返回
|
||||||
|
|
||||||
#### 2.2.2 实时数据动态显示功能
|
#### 2.2.2 实时数据动态显示功能
|
||||||
|
|
||||||
@ -65,7 +70,10 @@
|
|||||||
|
|
||||||
#### 2.2.4 报警事件管理功能
|
#### 2.2.4 报警事件管理功能
|
||||||
|
|
||||||
1. **报警数据采集**:实时获取嵌入式设备报警事件
|
1. **报警数据采集**:实时获取嵌入式设备报警事件,事件读取按新版 Modbus 协议执行:
|
||||||
|
- 轮询事件总寄存器 `0x00E5`
|
||||||
|
- 当事件个数 `n > 0` 时,读取输入寄存器 `0x0100` 开始的 `5 * n` 个事件寄存器
|
||||||
|
- 事件解析完成后,由后端回告本次成功读取数量
|
||||||
2. **数据存储**:报警事件保存至**SQLite数据库**
|
2. **数据存储**:报警事件保存至**SQLite数据库**
|
||||||
3. **展示方式**:**WebSocket实时推送**报警至Web界面,支持弹窗提醒+历史查询
|
3. **展示方式**:**WebSocket实时推送**报警至Web界面,支持弹窗提醒+历史查询
|
||||||
|
|
||||||
@ -79,14 +87,17 @@
|
|||||||
|
|
||||||
#### 2.3.2 下行数据(Python服务→嵌入式系统)
|
#### 2.3.2 下行数据(Python服务→嵌入式系统)
|
||||||
|
|
||||||
- 配置数据:设备参数、通道配置、定值、对时、灯光参数
|
- 配置数据:
|
||||||
|
- 通过 Modbus 下发:通道配置、线路定值/变比、AI 报警、系统对时、开出控制
|
||||||
|
- 本地过渡存储:设备基础参数、网卡、串口、密码等协议未定义项
|
||||||
- 控制指令:开关量分/合控制信号
|
- 控制指令:开关量分/合控制信号
|
||||||
|
|
||||||
### 2.4 数据存储需求
|
### 2.4 数据存储需求
|
||||||
|
|
||||||
1. **JSON文件存储**:设备版本、通讯参数、密码、AI/AO配置、定值配置等**非实时参数**
|
1. **设备寄存器真值存储**:通道配置、线路定值、AI 报警、系统对时等协议已定义项,以 Modbus 设备寄存器值为真值来源
|
||||||
2. **SQLite数据库**:报警事件数据(支持历史查询)
|
2. **JSON文件过渡存储**:设备版本、网卡、串口、密码等协议未定义项,当前仍使用本地 JSON 作为兼容过渡
|
||||||
3. **动态数据**:仅内存缓存,不持久化存储
|
3. **SQLite数据库**:报警事件数据(支持历史查询)
|
||||||
|
4. **动态数据**:仅内存缓存,不持久化存储
|
||||||
|
|
||||||
### 2.5 性能需求
|
### 2.5 性能需求
|
||||||
|
|
||||||
@ -98,7 +109,7 @@
|
|||||||
### 2.6 通信服务需求
|
### 2.6 通信服务需求
|
||||||
|
|
||||||
1. **HTTP服务**:基于FastAPI提供**RESTful API**,用于参数配置、状态查询、报警查询、控制指令
|
1. **HTTP服务**:基于FastAPI提供**RESTful API**,用于参数配置、状态查询、报警查询、控制指令
|
||||||
2. **WebSocket服务**:基于FastAPI提供双工通信,用于**实时数据、报警数据主动推送**至Web界面
|
2. **WebSocket服务**:基于FastAPI提供双工通信,用于**实时数据、报警数据、设备时间主动推送**至Web界面
|
||||||
|
|
||||||
## 三、系统架构设计
|
## 三、系统架构设计
|
||||||
|
|
||||||
@ -137,8 +148,11 @@ Python FastAPI服务层(RESTful接口 + WebSocket推送 + 数据处理)
|
|||||||
采用**FastAPI + Uvicorn**搭建,提供**HTTP接口**和**WebSocket推送**双服务:
|
采用**FastAPI + Uvicorn**搭建,提供**HTTP接口**和**WebSocket推送**双服务:
|
||||||
|
|
||||||
1. **RESTful API模块**:处理配置提交、状态查询、报警查询、控制指令
|
1. **RESTful API模块**:处理配置提交、状态查询、报警查询、控制指令
|
||||||
2. **WebSocket推送模块**:0.5秒推送实时数据、实时报警数据到Web端
|
2. **WebSocket推送模块**:
|
||||||
3. **参数配置模块**:JSON文件读写、配置下发C程序
|
- `0.5` 秒推送实时数据
|
||||||
|
- 实时推送报警数据
|
||||||
|
- `1` 秒推送设备当前时间到独立时间通道
|
||||||
|
3. **参数配置模块**:按协议区分“设备寄存器回读/回写”和“本地 JSON 兼容逻辑”
|
||||||
4. **实时数据模块**:定时读取C程序数据,缓存并通过WebSocket推送
|
4. **实时数据模块**:定时读取C程序数据,缓存并通过WebSocket推送
|
||||||
5. **报警管理模块**:接收报警→存储SQLite→WebSocket推送前端
|
5. **报警管理模块**:接收报警→存储SQLite→WebSocket推送前端
|
||||||
6. **设备状态模块**:采集并缓存设备运行状态
|
6. **设备状态模块**:采集并缓存设备运行状态
|
||||||
@ -155,11 +169,11 @@ Python FastAPI服务层(RESTful接口 + WebSocket推送 + 数据处理)
|
|||||||
1. **实时/报警数据流向**
|
1. **实时/报警数据流向**
|
||||||
嵌入式C程序 → Python → WebSocket主动推送 → Web界面(0.5秒刷新)
|
嵌入式C程序 → Python → WebSocket主动推送 → Web界面(0.5秒刷新)
|
||||||
2. **参数配置流向**
|
2. **参数配置流向**
|
||||||
Web界面 → HTTP POST → Python → JSON存储 + 下发C程序
|
Web界面 → HTTP POST → Python → Modbus写下位机(协议已定义项)/ 本地JSON保存(协议未定义项)
|
||||||
3. **状态/查询流向**
|
3. **状态/查询流向**
|
||||||
Web界面 → HTTP GET → Python → 返回数据展示
|
Web界面 → HTTP GET → Python → 返回数据展示
|
||||||
4. **配置回显流向**
|
4. **配置回显流向**
|
||||||
Web界面进入设置页 → HTTP GET配置查询接口 → Python读取JSON配置 → 返回当前配置数据 → Web界面显示
|
Web界面进入设置页 → HTTP GET配置查询接口 → Python通过 Modbus 回读设备参数或读取本地 JSON 兼容配置 → 返回当前配置数据 → Web界面显示
|
||||||
|
|
||||||
## 四、接口详细设计(RESTful + WebSocket)
|
## 四、接口详细设计(RESTful + WebSocket)
|
||||||
|
|
||||||
@ -332,6 +346,7 @@ Python FastAPI服务层(RESTful接口 + WebSocket推送 + 数据处理)
|
|||||||
|
|
||||||
**作用**:读取设备基础配置,用于设备/网络配置界面打开时回显当前参数
|
**作用**:读取设备基础配置,用于设备/网络配置界面打开时回显当前参数
|
||||||
**参数**:无
|
**参数**:无
|
||||||
|
**当前数据来源**:本地 `device.json`(协议未定义寄存器,作为兼容过渡保留)
|
||||||
**返回data**:
|
**返回data**:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@ -379,6 +394,7 @@ Python FastAPI服务层(RESTful接口 + WebSocket推送 + 数据处理)
|
|||||||
#### 4. POST /api/config/device
|
#### 4. POST /api/config/device
|
||||||
|
|
||||||
**作用**:提交设备基础配置(版本、通讯、密码)
|
**作用**:提交设备基础配置(版本、通讯、密码)
|
||||||
|
**当前保存方式**:写本地 `device.json`(协议未定义寄存器,作为兼容过渡保留)
|
||||||
**请求body参数**:
|
**请求body参数**:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@ -443,6 +459,7 @@ Python FastAPI服务层(RESTful接口 + WebSocket推送 + 数据处理)
|
|||||||
#### 5. GET /api/config/device/net
|
#### 5. GET /api/config/device/net
|
||||||
|
|
||||||
**作用**:按网卡标识读取单个网卡配置
|
**作用**:按网卡标识读取单个网卡配置
|
||||||
|
**当前数据来源**:本地 `device.json`
|
||||||
**参数**:
|
**参数**:
|
||||||
- `nic=网卡一`:按 `nic` 查询对应网卡信息
|
- `nic=网卡一`:按 `nic` 查询对应网卡信息
|
||||||
**返回data**:
|
**返回data**:
|
||||||
@ -462,6 +479,7 @@ Python FastAPI服务层(RESTful接口 + WebSocket推送 + 数据处理)
|
|||||||
#### 6. POST /api/config/device/net
|
#### 6. POST /api/config/device/net
|
||||||
|
|
||||||
**作用**:按网卡标识保存单个网卡配置
|
**作用**:按网卡标识保存单个网卡配置
|
||||||
|
**当前保存方式**:写本地 `device.json`
|
||||||
**请求body参数**:
|
**请求body参数**:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@ -485,6 +503,7 @@ Python FastAPI服务层(RESTful接口 + WebSocket推送 + 数据处理)
|
|||||||
#### 7. GET /api/config/device/uart
|
#### 7. GET /api/config/device/uart
|
||||||
|
|
||||||
**作用**:按串口标识读取单个串口配置
|
**作用**:按串口标识读取单个串口配置
|
||||||
|
**当前数据来源**:本地 `device.json`
|
||||||
**参数**:
|
**参数**:
|
||||||
- `port=COM1`:按 `port` 查询对应串口信息
|
- `port=COM1`:按 `port` 查询对应串口信息
|
||||||
**返回data**:
|
**返回data**:
|
||||||
@ -505,6 +524,7 @@ Python FastAPI服务层(RESTful接口 + WebSocket推送 + 数据处理)
|
|||||||
#### 8. POST /api/config/device/uart
|
#### 8. POST /api/config/device/uart
|
||||||
|
|
||||||
**作用**:按串口标识保存单个串口配置
|
**作用**:按串口标识保存单个串口配置
|
||||||
|
**当前保存方式**:写本地 `device.json`
|
||||||
**请求body参数**:
|
**请求body参数**:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@ -530,6 +550,7 @@ Python FastAPI服务层(RESTful接口 + WebSocket推送 + 数据处理)
|
|||||||
|
|
||||||
**作用**:读取通道配置,用于通道配置界面打开时回显当前参数
|
**作用**:读取通道配置,用于通道配置界面打开时回显当前参数
|
||||||
**参数**:无
|
**参数**:无
|
||||||
|
**当前数据来源**:后端通过 Modbus 从下位机回读保持寄存器参数区,不再读取 `channel.json`
|
||||||
**返回data**:
|
**返回data**:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@ -544,6 +565,7 @@ Python FastAPI服务层(RESTful接口 + WebSocket推送 + 数据处理)
|
|||||||
#### 10. POST /api/config/channel
|
#### 10. POST /api/config/channel
|
||||||
|
|
||||||
**作用**:提交AI/AO通道配置(AI:12通道,AO:12通道)
|
**作用**:提交AI/AO通道配置(AI:12通道,AO:12通道)
|
||||||
|
**当前保存方式**:后端仅通过 Modbus 写下位机,不再写 `channel.json`
|
||||||
**请求body参数**:
|
**请求body参数**:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@ -553,7 +575,7 @@ Python FastAPI服务层(RESTful接口 + WebSocket推送 + 数据处理)
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**返回data**:`{"save_path":"/config/channel.json","send_status":"成功"}`
|
**返回data**:`{"send_status":"成功","target":"channel","modbus_written":{"ai_registers":30,"ao_registers":30}}`
|
||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
@ -562,6 +584,7 @@ Python FastAPI服务层(RESTful接口 + WebSocket推送 + 数据处理)
|
|||||||
**作用**:读取线路报警设置,用于报警设置界面打开时回显当前线路参数
|
**作用**:读取线路报警设置,用于报警设置界面打开时回显当前线路参数
|
||||||
**参数**:
|
**参数**:
|
||||||
- `line_no=1`:按线路号读取对应线路的报警设置
|
- `line_no=1`:按线路号读取对应线路的报警设置
|
||||||
|
**当前数据来源**:后端通过 Modbus 从线路定值寄存器区回读,不再读取 `setting.json`
|
||||||
**返回data**:
|
**返回data**:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@ -579,9 +602,29 @@ Python FastAPI服务层(RESTful接口 + WebSocket推送 + 数据处理)
|
|||||||
"fault_alarm": [
|
"fault_alarm": [
|
||||||
{
|
{
|
||||||
"category": "PT断线",
|
"category": "PT断线",
|
||||||
|
"limit": 0,
|
||||||
"delay": 180,
|
"delay": 180,
|
||||||
"output_node": "开出1",
|
"output_node": "开出1",
|
||||||
"enabled": true
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "CT断线",
|
||||||
|
"limit": 0,
|
||||||
|
"delay": 180,
|
||||||
|
"output_node": "开出2",
|
||||||
|
"enabled": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"transformer_change": [
|
||||||
|
{
|
||||||
|
"category": "PT变化",
|
||||||
|
"value": 100.0,
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "CT变化",
|
||||||
|
"value": 50.0,
|
||||||
|
"enabled": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -592,6 +635,7 @@ Python FastAPI服务层(RESTful接口 + WebSocket推送 + 数据处理)
|
|||||||
#### 12. POST /api/config/line\_alarm\_setting
|
#### 12. POST /api/config/line\_alarm\_setting
|
||||||
|
|
||||||
**作用**:提交定值报警阈值配置
|
**作用**:提交定值报警阈值配置
|
||||||
|
**当前保存方式**:后端仅通过 Modbus 写下位机,不再写 `setting.json`
|
||||||
**请求body参数**:
|
**请求body参数**:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@ -637,26 +681,39 @@ Python FastAPI服务层(RESTful接口 + WebSocket推送 + 数据处理)
|
|||||||
"fault_alarm": [
|
"fault_alarm": [
|
||||||
{
|
{
|
||||||
"category": "PT断线",
|
"category": "PT断线",
|
||||||
|
"limit": 0,
|
||||||
"delay": 180,
|
"delay": 180,
|
||||||
"output_node": "开出1",
|
"output_node": "开出1",
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"category": "CT断线",
|
"category": "CT断线",
|
||||||
|
"limit": 0,
|
||||||
"delay": 180,
|
"delay": 180,
|
||||||
"output_node": "开出1",
|
"output_node": "开出1",
|
||||||
"enabled": false
|
"enabled": false
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"transformer_change": [
|
||||||
|
{
|
||||||
|
"category": "PT变化",
|
||||||
|
"value": 20,
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "CT变化",
|
||||||
|
"value": 30,
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**返回data**:`{"save_path":"/config/setting.json","send_status":"成功"}`
|
**返回data**:`{"send_status":"成功","target":"line_alarm","line_no":1,"modbus_written":16}`
|
||||||
|
|
||||||
说明:
|
说明:
|
||||||
- `setting.json` 中的 `line_alarm_setting` 按数组对象存储
|
- `transformer_change` 中的 `PT变化`、`CT变化` 参与一次值换算
|
||||||
- 保存接口按单条线路配置提交
|
- 每条线路按 16 个寄存器编码:7 类规则 + 2 个变比参数
|
||||||
- 服务端会根据 `line_no` 对数组中的对应线路进行覆盖更新,不存在时追加
|
|
||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
@ -664,6 +721,7 @@ Python FastAPI服务层(RESTful接口 + WebSocket推送 + 数据处理)
|
|||||||
|
|
||||||
**作用**:读取AI报警设置,用于报警设置界面打开时回显当前AI报警参数
|
**作用**:读取AI报警设置,用于报警设置界面打开时回显当前AI报警参数
|
||||||
**参数**:无
|
**参数**:无
|
||||||
|
**当前数据来源**:后端通过 Modbus 从 AI 报警寄存器区回读,不再读取 `setting.json`
|
||||||
**返回data**:
|
**返回data**:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@ -685,6 +743,7 @@ Python FastAPI服务层(RESTful接口 + WebSocket推送 + 数据处理)
|
|||||||
#### 14. POST /api/config/ai\_alarm\_setting
|
#### 14. POST /api/config/ai\_alarm\_setting
|
||||||
|
|
||||||
**作用**:提交AI报警设置
|
**作用**:提交AI报警设置
|
||||||
|
**当前保存方式**:后端仅通过 Modbus 写下位机,不再写 `setting.json`
|
||||||
**请求body参数**:
|
**请求body参数**:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@ -710,17 +769,18 @@ Python FastAPI服务层(RESTful接口 + WebSocket推送 + 数据处理)
|
|||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
**返回data**:`{"save_path":"/config/setting.json","send_status":"成功"}`
|
**返回data**:`{"send_status":"成功","target":"ai_alarm","items":2,"modbus_written":30}`
|
||||||
|
|
||||||
#### 15. GET /api/config/system
|
#### 15. GET /api/config/system
|
||||||
|
|
||||||
**作用**:读取系统设置,用于系统设置界面打开时回显当前参数
|
**作用**:读取系统设置,用于系统设置界面打开时回显当前参数
|
||||||
**参数**:无
|
**参数**:无
|
||||||
|
**当前数据来源**:对时参数从设备回读;`brightness`、`screen_saver` 当前仅保留接口兼容字段
|
||||||
**返回data**:
|
**返回data**:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"time_sync": "auto",
|
"time_sync": "2026-05-25 12:34:56",
|
||||||
"brightness": 80,
|
"brightness": 80,
|
||||||
"screen_saver": 60
|
"screen_saver": 60
|
||||||
}
|
}
|
||||||
@ -731,21 +791,68 @@ Python FastAPI服务层(RESTful接口 + WebSocket推送 + 数据处理)
|
|||||||
#### 16. POST /api/config/system
|
#### 16. POST /api/config/system
|
||||||
|
|
||||||
**作用**:提交系统对时、灯光配置
|
**作用**:提交系统对时、灯光配置
|
||||||
|
**当前保存方式**:仅 `time_sync` 通过 Modbus 下发;`brightness`、`screen_saver` 当前无协议寄存器,作为兼容字段保留但不下发
|
||||||
**请求body参数**:
|
**请求body参数**:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"time_sync": "auto",
|
|
||||||
"brightness": 80,
|
"brightness": 80,
|
||||||
"screen_saver": 60
|
"screen_saver": 60
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**返回data**:`{"send_status":"成功"}`
|
**返回data**:`{"send_status":"成功","target":"system","ignored_fields":["brightness","screen_saver"]}`
|
||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
#### 17. GET /api/alarm/list
|
#### 17. GET /api/config/time-sync
|
||||||
|
|
||||||
|
**作用**:读取设备当前时间,用于对时页面或时间显示模块初始化回显
|
||||||
|
**参数**:无
|
||||||
|
**当前数据来源**:后端通过 Modbus 从设备时间寄存器回读
|
||||||
|
**返回data**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"time_sync": "2026-05-25 14:30:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
#### 18. POST /api/config/time-sync
|
||||||
|
|
||||||
|
**作用**:单独调用设备对时接口,将指定时间写入下位机
|
||||||
|
**请求body参数**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"time_sync": "2026-05-25 14:30:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**返回data**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"send_status": "成功",
|
||||||
|
"target": "time_sync",
|
||||||
|
"time_sync": "2026-05-25 14:30:00",
|
||||||
|
"modbus_written": {
|
||||||
|
"time_registers": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 该接口与 `/api/config/system` 并行存在,职责更单一,只负责设备对时。
|
||||||
|
- 写入目标为时间设置寄存器 `0x0000` 开始的 `2` 个保持寄存器。
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
#### 19. GET /api/alarm/list
|
||||||
|
|
||||||
**作用**:分页查询历史报警
|
**作用**:分页查询历史报警
|
||||||
**参数**:
|
**参数**:
|
||||||
@ -787,7 +894,7 @@ GET /api/alarm/list?page=1&size=20&no=L1&type=current&start_time=2026-05-16T10:3
|
|||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
#### 18. POST /api/control/switch
|
#### 20. POST /api/control/switch
|
||||||
|
|
||||||
**作用**:下发开关量控制指令
|
**作用**:下发开关量控制指令
|
||||||
**请求body参数**:
|
**请求body参数**:
|
||||||
@ -961,12 +1068,41 @@ GET /api/alarm/list?page=1&size=20&no=L1&type=current&start_time=2026-05-16T10:3
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 3. WebSocket 地址:`ws://ip:port/ws/time-sync`
|
||||||
|
|
||||||
|
**推送内容**:设备当前时间(`1` 秒/次)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "time_sync",
|
||||||
|
"data": {
|
||||||
|
"time_sync": "2026-05-25 14:30:01"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 后台服务每 `1` 秒轮询一次设备时间寄存器。
|
||||||
|
- 轮询结果通过独立通道 `/ws/time-sync` 推送。
|
||||||
|
- 该通道与 `/ws/real-time`、`/ws/alarm` 相互独立,便于前端单独订阅设备时间。
|
||||||
|
|
||||||
## 五、数据存储设计
|
## 五、数据存储设计
|
||||||
|
|
||||||
### 5.1 JSON配置文件
|
### 5.1 配置真值与本地配置文件
|
||||||
|
|
||||||
- 路径:`/config/`
|
- **设备寄存器真值**:
|
||||||
- 文件:`device.json`、`channel.json`、`setting.json`
|
- `channel`
|
||||||
|
- `line_alarm_setting`
|
||||||
|
- `ai_alarm_setting`
|
||||||
|
- `system.time_sync`
|
||||||
|
- **本地 JSON 兼容配置**:
|
||||||
|
- 路径:`/config/`
|
||||||
|
- 文件:`device.json`、`channel.json`、`setting.json`
|
||||||
|
- 当前仅协议未定义寄存器的接口继续依赖本地 JSON,例如设备基础参数、网卡、串口、密码等
|
||||||
|
- **说明**:
|
||||||
|
- `channel.json`、`setting.json` 不再作为协议已定义配置项的运行期真值来源
|
||||||
|
- 协议已定义项由下位机寄存器回读结果决定,保存时也只写下位机
|
||||||
|
|
||||||
### 5.2 SQLite报警数据库
|
### 5.2 SQLite报警数据库
|
||||||
|
|
||||||
@ -994,8 +1130,9 @@ GET /api/alarm/list?page=1&size=20&no=L1&type=current&start_time=2026-05-16T10:3
|
|||||||
本方案基于**RK3568平台**,采用**FastAPI + HTTP + WebSocket**架构,满足电气量测控平台全部需求:
|
本方案基于**RK3568平台**,采用**FastAPI + HTTP + WebSocket**架构,满足电气量测控平台全部需求:
|
||||||
|
|
||||||
- RESTful API 负责配置、查询、控制
|
- RESTful API 负责配置、查询、控制
|
||||||
- WebSocket 负责实时数据、报警数据主动推送
|
- WebSocket 负责实时数据、报警数据、设备时间主动推送
|
||||||
- JSON 存储配置,SQLite 存储报警
|
- 协议已定义配置项以设备寄存器为真值,协议未定义项暂用本地 JSON 兼容
|
||||||
|
- SQLite 存储报警
|
||||||
- 0.5秒实时刷新,界面无卡顿、数据不丢失
|
- 0.5秒实时刷新,界面无卡顿、数据不丢失
|
||||||
|
|
||||||
系统架构轻量化、高性能,完全适配嵌入式平台运行。
|
系统架构轻量化、高性能,完全适配嵌入式平台运行。
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user