From 88ab0ca1ab219d3d90f270450069f58d883244b7 Mon Sep 17 00:00:00 2001 From: root <13910913995@163.com> Date: Tue, 19 May 2026 09:26:51 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8E=E7=AB=AF=E4=BB=A3=E7=A0=81=E6=9B=B4?= =?UTF-8?q?=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/routes/config.py | 22 +++- backend/app/services/platform_service.py | 119 ++++++++++++++++-- backend/config/channel.json | 2 +- backend/config/device.json | 10 +- backend/config/setting.json | 83 ++++++++---- backend/tests/test_api.py | 90 +++++++++++++ ...气量测控平台系统需求分析及技术框架设计.md | 116 +++++++++++++++-- frontend/src/views/AlarmHistoryView.vue | 2 +- frontend/src/views/AlarmSettingView.vue | 2 +- frontend/src/views/ChannelConfigView.vue | 2 +- frontend/src/views/ControlView.vue | 2 +- frontend/src/views/DeviceConfigView.vue | 2 +- frontend/src/views/StatusView.vue | 2 +- 13 files changed, 395 insertions(+), 59 deletions(-) diff --git a/backend/app/api/routes/config.py b/backend/app/api/routes/config.py index ca517ae..e9ecb99 100644 --- a/backend/app/api/routes/config.py +++ b/backend/app/api/routes/config.py @@ -4,7 +4,7 @@ from fastapi import APIRouter, Depends, Query from app.core.response import success_response from app.core.security import verify_api_token -from app.schemas.platform import AiAlarmSettingIn, ChannelConfigIn, DeviceConfigIn, LineAlarmSettingIn, SystemConfigIn +from app.schemas.platform import AiAlarmSettingIn, ChannelConfigIn, DeviceConfigIn, LineAlarmSettingIn, NetConfigItem, SystemConfigIn, UartConfigItem from app.services.platform_service import platform_service @@ -21,6 +21,26 @@ def save_device_config(payload: DeviceConfigIn) -> Dict[str, Any]: return success_response(platform_service.save_device_config(payload)) +@router.get("/device/net", dependencies=[Depends(verify_api_token)]) +def get_net_config(nic: str = Query(..., min_length=1)) -> Dict[str, Any]: + return success_response(platform_service.get_net_config(nic=nic), msg="获取网卡配置成功") + + +@router.post("/device/net", dependencies=[Depends(verify_api_token)]) +def save_net_config(payload: NetConfigItem) -> Dict[str, Any]: + return success_response(platform_service.save_net_config(payload), msg="保存网卡配置成功") + + +@router.get("/device/uart", dependencies=[Depends(verify_api_token)]) +def get_uart_config(port: str = Query(..., min_length=1)) -> Dict[str, Any]: + return success_response(platform_service.get_uart_config(port=port), msg="获取串口配置成功") + + +@router.post("/device/uart", dependencies=[Depends(verify_api_token)]) +def save_uart_config(payload: UartConfigItem) -> Dict[str, Any]: + return success_response(platform_service.save_uart_config(payload), msg="保存串口配置成功") + + @router.get("/channel", dependencies=[Depends(verify_api_token)]) def get_channel_config() -> Dict[str, Any]: return success_response(platform_service.get_channel_config(), msg="获取通道配置成功") diff --git a/backend/app/services/platform_service.py b/backend/app/services/platform_service.py index 791dc14..3548393 100644 --- a/backend/app/services/platform_service.py +++ b/backend/app/services/platform_service.py @@ -15,9 +15,11 @@ from app.schemas.platform import ( DeviceConfigIn, DeviceStatus, LineAlarmSettingIn, + NetConfigItem, RealtimeData, SwitchControlIn, SystemConfigIn, + UartConfigItem, ) from app.ws.manager import ws_manager @@ -44,16 +46,8 @@ class PlatformService: memory_store.set_status(status) return status - def save_device_config(self, payload: DeviceConfigIn) -> Dict[str, Any]: - data = payload.model_dump() - data["password"] = hash_password(payload.password) - path = self.config_repo.write_device_config(data) - device_result = self.device_client.send_device_config(payload) - return {"save_path": f"/config/{path.name}", **device_result} - - def get_device_config(self) -> Dict[str, Any]: - current = self.config_repo.read_device_config() - data = { + def _default_device_config(self) -> Dict[str, Any]: + return { "password": "", "hardware_version": { "board_version": "B001.001.001", @@ -74,11 +68,114 @@ class PlatformService: {"port": "COM2", "baud": 115200, "parity": "NONE", "data_bits": 8, "stop_bits": 1, "protocol": "Modbus RTU"}, ], } - data.update(current) + + def _merge_keyed_items(self, defaults: List[Dict[str, Any]], current: Any, key_field: str) -> List[Dict[str, Any]]: + merged = [dict(item) for item in defaults] + if not isinstance(current, list): + return merged + + index_map = { + item.get(key_field): index + for index, item in enumerate(merged) + if isinstance(item, dict) and item.get(key_field) is not None + } + for item in current: + if not isinstance(item, dict): + continue + key = item.get(key_field) + if key in index_map: + merged[index_map[key]].update(item) + else: + merged.append(dict(item)) + return merged + + def _upsert_keyed_item(self, items: List[Dict[str, Any]], payload: Dict[str, Any], key_field: str) -> List[Dict[str, Any]]: + key = payload.get(key_field) + updated = False + for index, item in enumerate(items): + if item.get(key_field) == key: + items[index] = payload + updated = True + break + if not updated: + items.append(payload) + return items + + def _find_keyed_item(self, items: List[Dict[str, Any]], key_field: str, key_value: str) -> Optional[Dict[str, Any]]: + for item in items: + if item.get(key_field) == key_value: + return item + return None + + def _load_device_config_with_defaults(self) -> Dict[str, Any]: + current = self.config_repo.read_device_config() + defaults = self._default_device_config() + data = { + "password": current.get("password", "") if isinstance(current, dict) else "", + "hardware_version": dict(defaults["hardware_version"]), + "software_version": dict(defaults["software_version"]), + "net": [], + "uart": [], + } + + if isinstance(current, dict): + if isinstance(current.get("hardware_version"), dict): + data["hardware_version"].update(current["hardware_version"]) + if isinstance(current.get("software_version"), dict): + data["software_version"].update(current["software_version"]) + data["net"] = self._merge_keyed_items(defaults["net"], current.get("net"), "nic") + data["uart"] = self._merge_keyed_items(defaults["uart"], current.get("uart"), "port") + else: + data["net"] = self._merge_keyed_items(defaults["net"], None, "nic") + data["uart"] = self._merge_keyed_items(defaults["uart"], None, "port") + + return data + + def save_device_config(self, payload: DeviceConfigIn) -> Dict[str, Any]: + data = self._load_device_config_with_defaults() + payload_data = payload.model_dump() + data["hardware_version"] = payload_data["hardware_version"] + data["software_version"] = payload_data["software_version"] + data["net"] = self._merge_keyed_items(data["net"], payload_data["net"], "nic") + data["uart"] = self._merge_keyed_items(data["uart"], payload_data["uart"], "port") + if payload.password.strip(): + data["password"] = hash_password(payload.password) + path = self.config_repo.write_device_config(data) + device_result = self.device_client.send_device_config(payload) + return {"save_path": f"/config/{path.name}", **device_result} + + def get_device_config(self) -> Dict[str, Any]: + data = self._load_device_config_with_defaults() # 不返回已保存的哈希密码,避免前端把哈希串直接显示到设置界面 data["password"] = "" return data + def get_net_config(self, nic: str) -> Dict[str, Any]: + device_config = self._load_device_config_with_defaults() + item = self._find_keyed_item(device_config["net"], "nic", nic) + if item is not None: + return item + return {"nic": nic, "ip": "", "mask": "", "gateway": "", "protocol": ""} + + def save_net_config(self, payload: NetConfigItem) -> Dict[str, Any]: + device_config = self._load_device_config_with_defaults() + device_config["net"] = self._upsert_keyed_item(device_config["net"], payload.model_dump(), "nic") + path = self.config_repo.write_device_config(device_config) + return {"save_path": f"/config/{path.name}", "target": "net", "nic": payload.nic, "send_status": "成功"} + + def get_uart_config(self, port: str) -> Dict[str, Any]: + device_config = self._load_device_config_with_defaults() + item = self._find_keyed_item(device_config["uart"], "port", port) + if item is not None: + return item + return {"port": port, "baud": 9600, "parity": "NONE", "data_bits": 8, "stop_bits": 1, "protocol": ""} + + def save_uart_config(self, payload: UartConfigItem) -> Dict[str, Any]: + device_config = self._load_device_config_with_defaults() + device_config["uart"] = self._upsert_keyed_item(device_config["uart"], payload.model_dump(), "port") + path = self.config_repo.write_device_config(device_config) + return {"save_path": f"/config/{path.name}", "target": "uart", "port": payload.port, "send_status": "成功"} + 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) diff --git a/backend/config/channel.json b/backend/config/channel.json index 2043af2..8b66336 100644 --- a/backend/config/channel.json +++ b/backend/config/channel.json @@ -3,7 +3,7 @@ { "ch": 1, "singal_type": "1-5v", - "line_no": 2, + "line_no": 1, "type": "UA", "limit_low": 0.0, "limit_high": 20.0 diff --git a/backend/config/device.json b/backend/config/device.json index c501daf..add360f 100644 --- a/backend/config/device.json +++ b/backend/config/device.json @@ -1,7 +1,7 @@ { - "password": "8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92", + "password": "ac9689e2272427085e35b9d3e3e8bed88cb3434828b43b86fc0596cad4c6e270", "hardware_version": { - "board_version": "B001.001.002", + "board_version": "B001.001.001", "display_version": "S001.001.001", "other_version": "Y001.001.001" }, @@ -29,15 +29,15 @@ "uart": [ { "port": "COM1", - "baud": 9600, + "baud": 19200, "parity": "NONE", "data_bits": 8, "stop_bits": 1, - "protocol": "" + "protocol": "Modbus RTU" }, { "port": "COM2", - "baud": 115200, + "baud": 19200, "parity": "NONE", "data_bits": 8, "stop_bits": 1, diff --git a/backend/config/setting.json b/backend/config/setting.json index 8960fed..b093bd8 100644 --- a/backend/config/setting.json +++ b/backend/config/setting.json @@ -1,29 +1,62 @@ { - "line_alarm_setting": - [ + "line_alarm_setting": [ { - "line_no": 1, - "over_limit_alarm": [ - { - "category": "??", - "limit": 180.0, - "delay": 180, - "output_node": "??1", - "enabled": true - } - ], - "fault_alarm": [ - { - "category": "PT??", - "limit": null, - "delay": 180, - "output_node": "??1", - "enabled": true - } - ] - } - ] - , + "line_no": 1, + "over_limit_alarm": [ + { + "category": "电压", + "limit": 0.0, + "delay": 0, + "output_node": "开出1", + "enabled": true + }, + { + "category": "电流", + "limit": 0.0, + "delay": 0, + "output_node": "开出2", + "enabled": true + }, + { + "category": "差流", + "limit": 0.0, + "delay": 0, + "output_node": "", + "enabled": false + }, + { + "category": "功率", + "limit": 0.0, + "delay": 0, + "output_node": "", + "enabled": false + }, + { + "category": "频率", + "limit": 0.0, + "delay": 0, + "output_node": "", + "enabled": false + } + ], + "fault_alarm": [ + { + "category": "PT断线", + "limit": 0.0, + "delay": 0, + "output_node": "", + "enabled": false + }, + { + "category": "CT断线", + "limit": 0.0, + "delay": 0, + "output_node": "", + "enabled": false + } + ] + } + ], "ai_alarm_setting": [ { "channel_no": 1, @@ -135,7 +168,7 @@ } ], "system_config": { - "time_sync": "manual", + "time_sync": "2026-05-18 17:05:28", "brightness": 83, "screen_saver": 120 } diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py index 8ccfb5e..79995d2 100644 --- a/backend/tests/test_api.py +++ b/backend/tests/test_api.py @@ -124,12 +124,16 @@ def test_config_query_endpoints(tmp_path) -> None: headers = {"X-API-Token": settings.auth_password} device_response = client.get("/api/config/device", headers=headers) + 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"}) 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}) ai_alarm_response = client.get("/api/config/ai_alarm_setting", headers=headers) system_response = client.get("/api/config/system", headers=headers) assert device_response.status_code == 200 + assert net_response.status_code == 200 + assert uart_response.status_code == 200 assert channel_response.status_code == 200 assert line_alarm_response.status_code == 200 assert ai_alarm_response.status_code == 200 @@ -137,6 +141,8 @@ def test_config_query_endpoints(tmp_path) -> None: assert device_response.json()["data"]["password"] == "" assert device_response.json()["data"]["net"][0]["ip"] == "192.168.1.10" + assert net_response.json()["data"]["nic"] == "网卡一" + assert uart_response.json()["data"]["port"] == "COM1" 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"]["over_limit_alarm"][0]["category"] == "电流" @@ -169,3 +175,87 @@ def test_save_line_alarm_setting_stores_list(tmp_path) -> None: assert saved["line_alarm_setting"][0]["line_no"] == 3 finally: platform_service.config_repo = old_repo + + +def test_save_device_net_and_uart_by_key(tmp_path) -> None: + old_repo = platform_service.config_repo + platform_service.config_repo = JsonConfigRepository(tmp_path) + + try: + platform_service.config_repo.write_device_config( + { + "password": "hashed-password", + "hardware_version": { + "board_version": "B001.001.001", + "display_version": "S001.001.001", + "other_version": "Y001.001.001", + }, + "software_version": { + "display_program": "001.001.001", + "communication_program": "001.001.001", + "measurement_program": "001.001.001", + }, + "net": [ + {"nic": "网卡一", "ip": "192.168.1.10", "mask": "255.255.255.0", "gateway": "192.168.1.1", "protocol": "Modbus TCP"}, + {"nic": "网卡二", "ip": "192.168.1.56", "mask": "255.255.255.255", "gateway": "192.168.1.56", "protocol": "Modbus TCP"}, + ], + "uart": [ + {"port": "COM1", "baud": 9600, "parity": "NONE", "data_bits": 8, "stop_bits": 1, "protocol": ""}, + {"port": "COM2", "baud": 115200, "parity": "NONE", "data_bits": 8, "stop_bits": 1, "protocol": "Modbus RTU"}, + ], + } + ) + + headers = {"X-API-Token": settings.auth_password} + + net_save = client.post( + "/api/config/device/net", + headers=headers, + json={"nic": "网卡二", "ip": "10.10.10.2", "mask": "255.255.255.0", "gateway": "10.10.10.1", "protocol": "IEC104"}, + ) + uart_save = client.post( + "/api/config/device/uart", + headers=headers, + json={"port": "COM2", "baud": 4800, "parity": "EVEN", "data_bits": 8, "stop_bits": 1, "protocol": "DLT645"}, + ) + full_save = client.post( + "/api/config/device", + headers=headers, + json={ + "password": "", + "hardware_version": { + "board_version": "B001.001.003", + "display_version": "S001.001.001", + "other_version": "Y001.001.001", + }, + "software_version": { + "display_program": "001.001.001", + "communication_program": "001.001.001", + "measurement_program": "001.001.001", + }, + "net": [ + {"nic": "网卡一", "ip": "172.16.1.10", "mask": "255.255.255.0", "gateway": "172.16.1.1", "protocol": "Modbus TCP"} + ], + "uart": [ + {"port": "COM1", "baud": 19200, "parity": "ODD", "data_bits": 8, "stop_bits": 1, "protocol": "Modbus RTU"} + ], + }, + ) + + assert net_save.status_code == 200 + assert uart_save.status_code == 200 + assert full_save.status_code == 200 + + saved = platform_service.config_repo.read_device_config() + net_map = {item["nic"]: item for item in saved["net"]} + uart_map = {item["port"]: item for item in saved["uart"]} + + assert net_map["网卡一"]["ip"] == "172.16.1.10" + assert net_map["网卡二"]["ip"] == "10.10.10.2" + assert net_map["网卡二"]["protocol"] == "IEC104" + assert uart_map["COM1"]["baud"] == 19200 + assert uart_map["COM2"]["baud"] == 4800 + assert uart_map["COM2"]["protocol"] == "DLT645" + assert saved["password"] == "hashed-password" + finally: + platform_service.config_repo = old_repo diff --git a/document/电气量测控平台系统需求分析及技术框架设计.md b/document/电气量测控平台系统需求分析及技术框架设计.md index 1f03491..594541c 100644 --- a/document/电气量测控平台系统需求分析及技术框架设计.md +++ b/document/电气量测控平台系统需求分析及技术框架设计.md @@ -369,6 +369,11 @@ Python FastAPI服务层(RESTful接口 + WebSocket推送 + 数据处理) } ``` +说明: +- `net` 按 `nic` 标识各网卡配置 +- `uart` 按 `port` 标识各串口配置 +- 界面如需按单个网卡或单个串口精确读取,可调用后续独立查询接口 + *** #### 4. POST /api/config/device @@ -428,9 +433,100 @@ Python FastAPI服务层(RESTful接口 + WebSocket推送 + 数据处理) **返回data**:`{"save_path":"/config/device.json","send_status":"成功"}` +说明: +- 设备整体保存时,网卡配置按 `nic` 合并更新 +- 串口配置按 `port` 合并更新 +- 未出现在本次提交中的其他网卡或串口配置会保留,不会因数组顺序变化被误覆盖 + *** -#### 5. GET /api/config/channel +#### 5. GET /api/config/device/net + +**作用**:按网卡标识读取单个网卡配置 +**参数**: +- `nic=网卡一`:按 `nic` 查询对应网卡信息 +**返回data**: + +```json +{ + "nic": "网卡一", + "ip": "192.168.1.10", + "mask": "255.255.255.0", + "gateway": "192.168.1.1", + "protocol": "Modbus TCP" +} +``` + +*** + +#### 6. POST /api/config/device/net + +**作用**:按网卡标识保存单个网卡配置 +**请求body参数**: + +```json +{ + "nic": "网卡一", + "ip": "192.168.1.10", + "mask": "255.255.255.0", + "gateway": "192.168.1.1", + "protocol": "Modbus TCP" +} +``` + +**返回data**:`{"save_path":"/config/device.json","target":"net","nic":"网卡一","send_status":"成功"}` + +说明: +- 服务端会根据 `nic` 更新对应网卡对象 +- 若不存在相同 `nic`,则追加到 `net` 数组 + +*** + +#### 7. GET /api/config/device/uart + +**作用**:按串口标识读取单个串口配置 +**参数**: +- `port=COM1`:按 `port` 查询对应串口信息 +**返回data**: + +```json +{ + "port": "COM1", + "baud": 9600, + "parity": "NONE", + "data_bits": 8, + "stop_bits": 1, + "protocol": "Modbus RTU" +} +``` + +*** + +#### 8. POST /api/config/device/uart + +**作用**:按串口标识保存单个串口配置 +**请求body参数**: + +```json +{ + "port": "COM1", + "baud": 9600, + "parity": "NONE", + "data_bits": 8, + "stop_bits": 1, + "protocol": "Modbus RTU" +} +``` + +**返回data**:`{"save_path":"/config/device.json","target":"uart","port":"COM1","send_status":"成功"}` + +说明: +- 服务端会根据 `port` 更新对应串口对象 +- 若不存在相同 `port`,则追加到 `uart` 数组 + +*** + +#### 9. GET /api/config/channel **作用**:读取通道配置,用于通道配置界面打开时回显当前参数 **参数**:无 @@ -445,7 +541,7 @@ Python FastAPI服务层(RESTful接口 + WebSocket推送 + 数据处理) *** -#### 6. POST /api/config/channel +#### 10. POST /api/config/channel **作用**:提交AI/AO通道配置(AI:12通道,AO:12通道) **请求body参数**: @@ -461,7 +557,7 @@ Python FastAPI服务层(RESTful接口 + WebSocket推送 + 数据处理) *** -#### 7. GET /api/config/line\_alarm\_setting +#### 11. GET /api/config/line\_alarm\_setting **作用**:读取线路报警设置,用于报警设置界面打开时回显当前线路参数 **参数**: @@ -493,7 +589,7 @@ Python FastAPI服务层(RESTful接口 + WebSocket推送 + 数据处理) *** -#### 8. POST /api/config/line\_alarm\_setting +#### 12. POST /api/config/line\_alarm\_setting **作用**:提交定值报警阈值配置 **请求body参数**: @@ -564,7 +660,7 @@ Python FastAPI服务层(RESTful接口 + WebSocket推送 + 数据处理) *** -#### 9. GET /api/config/ai\_alarm\_setting +#### 13. GET /api/config/ai\_alarm\_setting **作用**:读取AI报警设置,用于报警设置界面打开时回显当前AI报警参数 **参数**:无 @@ -586,7 +682,7 @@ Python FastAPI服务层(RESTful接口 + WebSocket推送 + 数据处理) *** -#### 10. POST /api/config/ai\_alarm\_setting +#### 14. POST /api/config/ai\_alarm\_setting **作用**:提交AI报警设置 **请求body参数**: @@ -616,7 +712,7 @@ Python FastAPI服务层(RESTful接口 + WebSocket推送 + 数据处理) **返回data**:`{"save_path":"/config/setting.json","send_status":"成功"}` -#### 11. GET /api/config/system +#### 15. GET /api/config/system **作用**:读取系统设置,用于系统设置界面打开时回显当前参数 **参数**:无 @@ -632,7 +728,7 @@ Python FastAPI服务层(RESTful接口 + WebSocket推送 + 数据处理) *** -#### 12. POST /api/config/system +#### 16. POST /api/config/system **作用**:提交系统对时、灯光配置 **请求body参数**: @@ -649,7 +745,7 @@ Python FastAPI服务层(RESTful接口 + WebSocket推送 + 数据处理) *** -#### 13. GET /api/alarm/list +#### 17. GET /api/alarm/list **作用**:分页查询历史报警 **参数**: @@ -691,7 +787,7 @@ GET /api/alarm/list?page=1&size=20&no=L1&type=current&start_time=2026-05-16T10:3 *** -#### 14. POST /api/control/switch +#### 18. POST /api/control/switch **作用**:下发开关量控制指令 **请求body参数**: diff --git a/frontend/src/views/AlarmHistoryView.vue b/frontend/src/views/AlarmHistoryView.vue index a745dc2..288cb3a 100644 --- a/frontend/src/views/AlarmHistoryView.vue +++ b/frontend/src/views/AlarmHistoryView.vue @@ -1,4 +1,4 @@ - diff --git a/frontend/src/views/AlarmSettingView.vue b/frontend/src/views/AlarmSettingView.vue index c67aeea..c187513 100644 --- a/frontend/src/views/AlarmSettingView.vue +++ b/frontend/src/views/AlarmSettingView.vue @@ -1,4 +1,4 @@ -