电气量测控平台
+RK3568 + FastAPI + WebSocket + Vue 的前后端开发框架骨架。
+commit b8974dce59f8ae7069561cc183dfcf6536373c22 Author: root <13910913995@163.com> Date: Mon May 18 09:12:14 2026 +0800 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b7df8b9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,50 @@ +# Python +.venv/ +backend/.venv/ +Python/ +**/__pycache__/ +*.py[cod] +*.pyd +*.so +*.dll +*.dylib +*.egg-info/ +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.coverage +coverage.xml +htmlcov/ + +# Python packaging / caches +backend/.deps-cache/ +backend/.pip-cache/ +backend/.pip-cache-py38/ +backend/UNKNOWN.egg-info/ +backend/emcp_backend.egg-info/ +backend/.tmp_ws_check.py + +# Runtime data +backend/data/ + +# Environment files +.env +.env.* + +# Frontend +frontend/node_modules/ +frontend/dist/ +frontend/.vite/ +*.tsbuildinfo +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# IDE / OS +.vscode/ +.idea/ +*.swp +Thumbs.db +.DS_Store + diff --git a/.trae/skills/python-fullstack-scaffold/SKILL.md b/.trae/skills/python-fullstack-scaffold/SKILL.md new file mode 100644 index 0000000..fc7e24e --- /dev/null +++ b/.trae/skills/python-fullstack-scaffold/SKILL.md @@ -0,0 +1,454 @@ +--- +name: "python-fullstack-scaffold" +description: "为 FastAPI + WebSocket + Web 前端系统生成前后端开发框架。用户需要搭建或重构 RK3568 电气量测控平台的 Python 全栈代码结构时调用。" +--- + +# Python Fullstack Scaffold + +## 适用场景 + +当用户出现以下意图时,调用此技能: + +- 要根据需求文档搭建 `FastAPI + WebSocket + Web前端` 的项目骨架 +- 要为 `RK3568 / Ubuntu 20.04 / Python 3.8` 平台设计前后端分层结构 +- 要从接口文档反推后端路由、数据模型、配置存储和前端页面模块 +- 要统一实时数据、设备状态、报警事件、参数配置的代码组织方式 +- 要为嵌入式 C 程序与 Python 服务之间的适配层预留稳定接口 + +如果用户只是修一个接口、改一个组件、补一个测试,不要调用此技能。 + +## 技能目标 + +基于“电气量测控平台系统需求分析与架构设计技术方案”,输出一套可直接落地的前后端开发框架,覆盖: + +- 后端目录结构 +- 前端目录结构 +- 核心领域模型 +- RESTful API 路由分层 +- WebSocket 推送框架 +- JSON/SQLite 数据存储组织 +- 嵌入式适配层抽象 +- 开发顺序、验收点与最小可运行骨架 + +## 固定技术约束 + +在生成方案或代码时,默认遵循以下约束,除非用户明确要求变更: + +- 平台:`RK3568`,系统为 `Ubuntu 20.04` +- 后端:`Python 3.8`、`FastAPI`、`Uvicorn` +- 前端:Web 前端,优先采用当前项目已有技术栈;若项目未定型,则使用轻量、易部署的前端组织方式 +- 通信:`HTTP RESTful API + WebSocket` +- 配置存储:`JSON` +- 报警存储:`SQLite` +- 实时数据:仅内存缓存,不落盘 +- 实时刷新周期:`0.5 秒` +- 指令下发响应目标:`1 秒内` +- 系统需支持 `7 x 24` 连续运行 + +## 总体架构原则 + +始终按下面的职责边界组织代码: + +1. 硬件层:`RK3568 + AI/AO + 开关量 + 网络/串口 + 显示屏` +2. 嵌入式 C 程序层:采集、控制执行、状态监测、报警生成 +3. Python 服务层:接口、业务编排、缓存、存储、推送 +4. Web 界面层:配置、状态、实时数据、报警、控制 + +禁止把以下职责混在一起: + +- 路由层里直接写文件读写和数据库细节 +- WebSocket 推送逻辑散落在多个业务模块中 +- 前端页面组件里直接硬编码接口路径和协议细节 +- C 程序适配逻辑与业务规则耦合 + +## 后端推荐目录 + +当用户要求搭建后端框架时,优先生成或对齐为如下结构: + +```text +backend/ + app/ + main.py + core/ + config.py + logging.py + security.py + response.py + exceptions.py + api/ + deps.py + router.py + routes/ + realtime.py + status.py + config_device.py + config_channel.py + config_alarm.py + config_system.py + alarms.py + control.py + schemas/ + common.py + realtime.py + status.py + device_config.py + channel_config.py + alarm_setting.py + system_config.py + alarm_event.py + control.py + services/ + realtime_service.py + status_service.py + config_service.py + alarm_service.py + control_service.py + sync_service.py + repositories/ + json_config_repo.py + alarm_repo.py + adapters/ + c_device_client.py + mock_device_client.py + protocol_models.py + ws/ + manager.py + realtime_publisher.py + alarm_publisher.py + db/ + sqlite.py + models.py + cache/ + memory_store.py + tasks/ + polling.py + utils/ + time_utils.py + backup.py + config/ + device.json + channel.json + setting.json + data/ + alarm.db + tests/ +``` + +## 前端推荐目录 + +当用户要求搭建前端框架时,优先生成或对齐为如下结构: + +```text +frontend/ + src/ + main.ts + App.vue + router/ + index.ts + api/ + http.ts + realtime.ts + status.ts + config.ts + alarm.ts + control.ts + websocket/ + realtime.ts + alarm.ts + reconnect.ts + stores/ + realtime.ts + status.ts + alarm.ts + config.ts + views/ + RealtimeView.vue + StatusView.vue + DeviceConfigView.vue + ChannelConfigView.vue + AlarmSettingView.vue + AlarmHistoryView.vue + ControlView.vue + SystemConfigView.vue + components/ + status/ + realtime/ + alarm/ + config/ + control/ + types/ + api.ts + realtime.ts + status.ts + config.ts + alarm.ts + utils/ + format.ts + validators.ts +``` + +如果仓库已有前端目录,则优先复用现有技术栈、构建工具和状态管理方式,不要无意义重建。 + +## 领域模型拆分 + +构建代码框架时,必须先把需求拆成以下核心模型: + +### 1. 实时数据模型 + +- `line_list` +- `switch` +- `ai_collect` + +其中 `line_list` 需要体现: + +- `line_no` +- 一次值 `pri_val` +- 二次值 `sec_val` + +### 2. 设备状态模型 + +- `self_check` +- `net1` +- `net2` +- `uart1` +- `uart2` + +### 3. 设备基础配置模型 + +- 操作密码 +- 硬件版本信息 +- 软件版本信息 +- 网口配置 +- 串口配置 + +### 4. 通道配置模型 + +- `ai_channel` +- `ao_channel` + +### 5. 定值与 AI 报警设置模型 + +- 线路告警阈值 +- 故障告警设置 +- AI 通道报警设置 + +### 6. 系统配置模型 + +- 对时设置 +- 亮度设置 +- 屏保时间 + +### 7. 报警事件模型 + +- `alarm_type` +- `time` +- `no` +- `type` +- `content` +- `level` + +### 8. 控制指令模型 + +- `ch` +- `action` + +## API 设计规则 + +生成后端接口时,统一遵守以下规范: + +- 所有 HTTP 接口统一响应结构:`code`、`msg`、`data` +- 路由层仅负责参数校验、调用服务、返回标准响应 +- `GET /api/real-time-data` 作为实时数据备用查询接口 +- `GET /api/device-status` 用于设备状态读取 +- `POST /api/config/device` 持久化设备基础配置并下发设备 +- `POST /api/config/channel` 持久化 AI/AO 通道配置并下发设备 +- `POST /api/config/line_alarm_setting` 持久化线路定值与故障告警设置 +- `POST /api/config/ai_alarm_setting` 持久化 AI 告警设置 +- `POST /api/config/system` 处理对时、亮度、屏保配置,并实时下发 +- `GET /api/alarm/list` 分页查询报警历史 +- `POST /api/control/switch` 下发开关量控制指令 + +如果用户要求生成代码,优先先搭出路由、Schema、Service、Repository 的闭环,再补设备适配与前端页面。 + +## WebSocket 设计规则 + +必须把 WebSocket 当作一等公民设计,不要退化为轮询替代品。 + +### 实时数据通道 + +- 地址:`/ws/real-time` +- 推送周期:`0.5 秒` +- 推送内容:`real_time` +- 数据来源:内存缓存中的最新采样结果 + +### 报警通道 + +- 地址:`/ws/alarm` +- 触发方式:报警事件产生即推送 +- 推送内容:单条报警事件或标准化事件消息 + +### WebSocket 实现要求 + +- 统一连接管理器 +- 支持多客户端广播 +- 支持断线清理 +- 前端必须具备自动重连 +- 推送数据结构必须和前端类型定义一致 + +## 存储设计规则 + +### JSON 配置 + +以下配置默认存放在 `config/`: + +- `device.json` +- `channel.json` +- `setting.json` + +要求: + +- 保存前先做 Schema 校验 +- 写入时支持原子替换 +- 修改后自动备份旧版本 +- 关键字段变更后调用设备下发接口 + +### SQLite 报警库 + +报警事件单独存放在 SQLite 中,至少包含: + +- `id` +- `alarm_type` +- `time` +- `no` +- `type` +- `content` +- `level` + +要求: + +- 写入逻辑集中在 Repository +- 查询支持分页 +- 前端历史页调用分页接口,不直接访问数据库 + +### 内存缓存 + +以下数据只保存在内存中: + +- 实时量测数据 +- 实时设备状态 +- 最近待推送报警缓冲 + +## 嵌入式适配层规则 + +当用户要落地与 C 程序交互的代码框架时,必须建立设备适配层,不要让业务层直接依赖具体协议细节。 + +至少抽象以下能力: + +- 读取实时数据 +- 读取设备状态 +- 读取报警事件 +- 下发基础配置 +- 下发通道配置 +- 下发系统设置 +- 下发开关控制 + +推荐做法: + +- 定义统一客户端接口,例如 `DeviceClient` +- 提供 `MockDeviceClient` 供联调和前端开发使用 +- 提供 `CDeviceClient` 作为真实嵌入式对接实现 +- 通过配置切换 mock / real 模式 + +## 安全与可靠性规则 + +必须纳入以下要求: + +- 密码不得明文长期存储,至少做哈希处理 +- 配置接口增加基本权限校验能力 +- WebSocket 支持断线重连 +- 配置写入支持备份 +- 服务异常场景需易于接入守护或自动重启机制 + +## 推荐开发顺序 + +当用户要求“开始搭框架”时,按以下顺序推进: + +1. 先梳理接口、数据模型、目录结构 +2. 再生成后端基础工程:配置、日志、异常、统一响应 +3. 再生成 Schema、Repository、Service、Router +4. 再搭建内存缓存、轮询任务、WebSocket 管理器 +5. 再补 SQLite 与 JSON 持久化 +6. 再抽象设备适配层和 mock 数据源 +7. 最后生成前端 API 封装、状态管理、页面骨架与 WebSocket 客户端 + +## 输出要求 + +调用本技能后,输出内容应尽量包含以下几项: + +### 如果用户要“方案” + +输出: + +- 建议目录结构 +- 模块职责划分 +- 核心数据模型清单 +- API / WebSocket 对应关系 +- 开发阶段拆分 + +### 如果用户要“直接写代码” + +输出并执行: + +- 先识别现有仓库中 `backend/`、`frontend/` 的当前结构 +- 尽量增量修改,不要推倒重来 +- 先落最小可运行骨架,再补细节 +- 优先保证接口契约、模块边界、可测试性 + +## 生成代码时的验收清单 + +生成或修改后,至少检查: + +- 后端是否存在统一应用入口 +- 路由、服务、仓储、适配层是否职责分离 +- 是否覆盖需求文档中的全部 API +- 是否存在 `/ws/real-time` 与 `/ws/alarm` +- 实时数据是否只走内存缓存 +- 报警历史是否落 SQLite +- 配置是否落 JSON +- 前端是否拆出页面、API、WebSocket、类型定义 +- 是否保留 mock 联调能力 + +## 常用提示模板 + +### 模板 1:搭建完整框架 + +```text +请基于当前仓库和需求文档,搭建 RK3568 电气量测控平台的前后端代码框架。 +后端使用 FastAPI,前端沿用仓库现有技术栈。 +要求包含目录结构、统一响应、配置读写、SQLite 告警存储、WebSocket 实时推送、设备适配层与 mock 数据源。 +``` + +### 模板 2:只搭后端骨架 + +```text +请按 python-fullstack-scaffold 技能,为该项目生成后端骨架。 +要求覆盖 realtime、status、config、alarm、control 五类能力,并预留嵌入式 C 程序适配层。 +``` + +### 模板 3:只搭前端骨架 + +```text +请按 python-fullstack-scaffold 技能,为该项目生成前端页面骨架和 API/WebSocket 接入层。 +要求覆盖实时数据、设备状态、基础配置、通道配置、报警历史和控制页面。 +``` + +## 执行提醒 + +在实际执行前,先确认以下信息: + +- 当前仓库是否已经存在 `backend/` 和 `frontend/` +- 前端当前使用的具体技术栈和构建工具 +- 后端是否已有可复用入口、配置系统和测试结构 +- 用户要的是“技能文件”、 “设计方案” 还是 “直接生成代码” + +若信息不足,先补充上下文;若仓库已有实现,优先做兼容式演进,而不是重建。 diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..c01db09 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,16 @@ +# EMCP Backend + +## 启动 + +```bash +pip install -e .[dev] +uvicorn app.main:app --reload +``` + +## 说明 + +- 默认使用 `MockDeviceClient` 产生实时数据和模拟告警 +- 需要鉴权的接口使用请求头 `X-API-Token: admin123` +- WebSocket 通道: + - `/ws/real-time` + - `/ws/alarm` diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/backend/app/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/app/adapters/device_client.py b/backend/app/adapters/device_client.py new file mode 100644 index 0000000..e524523 --- /dev/null +++ b/backend/app/adapters/device_client.py @@ -0,0 +1,157 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from datetime import datetime +from random import Random +from typing import Any, Dict, List + +from app.schemas.platform import ( + AlarmEvent, + ChannelConfigIn, + DeviceConfigIn, + DeviceStatus, + LineAlarmSettingIn, + LineData, + RealtimeData, + SystemConfigIn, + SwitchControlIn, + ValueGroup, +) + + +class DeviceClient(ABC): + @abstractmethod + def read_realtime_data(self) -> RealtimeData: + raise NotImplementedError + + @abstractmethod + def read_device_status(self) -> DeviceStatus: + raise NotImplementedError + + @abstractmethod + def read_alarm_events(self) -> List[AlarmEvent]: + raise NotImplementedError + + @abstractmethod + def send_device_config(self, payload: DeviceConfigIn) -> Dict[str, Any]: + raise NotImplementedError + + @abstractmethod + def send_channel_config(self, payload: ChannelConfigIn) -> Dict[str, Any]: + raise NotImplementedError + + @abstractmethod + def send_line_alarm_setting(self, payload: LineAlarmSettingIn) -> Dict[str, Any]: + raise NotImplementedError + + @abstractmethod + def send_ai_alarm_setting(self, payload: List[Dict[str, Any]]) -> Dict[str, Any]: + raise NotImplementedError + + @abstractmethod + def send_system_config(self, payload: SystemConfigIn) -> Dict[str, Any]: + raise NotImplementedError + + @abstractmethod + def send_switch_control(self, payload: SwitchControlIn) -> Dict[str, Any]: + raise NotImplementedError + + +class MockDeviceClient(DeviceClient): + def __init__(self) -> None: + self._random = Random(3568) + self._tick = 0 + + def _value_group(self, base_u: float, base_i: float, base_p: float) -> ValueGroup: + delta = self._tick % 5 + return ValueGroup( + Ua=base_u + delta, + Ub=base_u + 2 + delta, + Uc=base_u - 1 + delta, + Ia=base_i + 0.1 * delta, + Ib=base_i - 0.1 + 0.1 * delta, + Ic=base_i + 0.2 + 0.1 * delta, + Pa=base_p + delta, + Pb=base_p - 2 + delta, + Pc=base_p + 3 + delta, + Pt=base_p * 3 + delta, + Qa=12 + delta, + Qb=11 + delta, + Qc=13 + delta, + Qt=36 + delta, + Sa=base_p + 15 + delta, + Sb=base_p + 10 + delta, + Sc=base_p + 16 + delta, + St=base_p * 3 + 44 + delta, + PFa=0.98, + PFb=0.97, + PFc=0.99, + PFt=0.98, + Uab=base_u * 1.73, + Ubc=base_u * 1.72, + Uca=base_u * 1.73, + frq=50.0, + ) + + def read_realtime_data(self) -> RealtimeData: + self._tick += 1 + line_list = [ + LineData(line_no=1, pri_val=self._value_group(6000, 150, 820), sec_val=self._value_group(57.6, 1.2, 68)), + LineData(line_no=2, pri_val=self._value_group(5990, 148, 810), sec_val=self._value_group(57.2, 1.1, 66)), + LineData(line_no=3, pri_val=self._value_group(6010, 151, 830), sec_val=self._value_group(58.1, 1.3, 70)), + LineData(line_no=4, pri_val=self._value_group(6020, 149, 825), sec_val=self._value_group(58.4, 1.2, 69)), + ] + switch = {f"di{i}": int((i + self._tick) % 2 == 0) for i in range(1, 13)} + switch.update({f"do{i}": int((i + self._tick + 1) % 2 == 0) for i in range(1, 13)}) + ai_collect = {f"ai{i}": round(1 + self._random.random() * 5 + (self._tick % 3) * 0.1, 2) for i in range(1, 13)} + return RealtimeData(line_list=line_list, switch=switch, ai_collect=ai_collect) + + def read_device_status(self) -> DeviceStatus: + return DeviceStatus( + self_check="正常", + net1="正常", + net2="正常", + uart1="正常", + uart2="正常" if self._tick % 4 else "断开", + ) + + def read_alarm_events(self) -> List[AlarmEvent]: + if self._tick % 6 != 0: + return [] + return [ + AlarmEvent( + alarm_type="line_alarm", + time=datetime.now(), + no=str((self._tick // 6) % 4 + 1), + type="电压", + content="模拟告警:线路电压越限", + level="中", + ) + ] + + def send_device_config(self, payload: DeviceConfigIn) -> Dict[str, Any]: + return {"send_status": "成功", "target": "device", "items": len(payload.net) + len(payload.uart)} + + def send_channel_config(self, payload: ChannelConfigIn) -> Dict[str, Any]: + return { + "send_status": "成功", + "target": "channel", + "items": len(payload.ai_channel) + len(payload.ao_channel), + } + + def send_line_alarm_setting(self, payload: LineAlarmSettingIn) -> Dict[str, Any]: + 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]: + return {"send_status": "成功", "target": "ai_alarm", "items": len(payload)} + + def send_system_config(self, payload: SystemConfigIn) -> Dict[str, Any]: + return {"send_status": "成功", "target": "system", "brightness": payload.brightness} + + def send_switch_control(self, payload: SwitchControlIn) -> Dict[str, Any]: + action_text = "合" if payload.action == 1 else "分" + return {"control_status": f"执行成功: 开关{payload.ch}{action_text}"} + + +class CDeviceClient(MockDeviceClient): + """真实 C 程序接入前先复用 mock 行为,后续在此类中替换协议实现。""" diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/backend/app/api/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/app/api/router.py b/backend/app/api/router.py new file mode 100644 index 0000000..e4d86de --- /dev/null +++ b/backend/app/api/router.py @@ -0,0 +1,12 @@ +from fastapi import APIRouter + +from app.api.routes import alarms, auth, config, control, realtime, status + + +api_router = APIRouter() +api_router.include_router(realtime.router) +api_router.include_router(status.router) +api_router.include_router(auth.router) +api_router.include_router(config.router) +api_router.include_router(alarms.router) +api_router.include_router(control.router) diff --git a/backend/app/api/routes/__init__.py b/backend/app/api/routes/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/backend/app/api/routes/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/app/api/routes/alarms.py b/backend/app/api/routes/alarms.py new file mode 100644 index 0000000..d94452e --- /dev/null +++ b/backend/app/api/routes/alarms.py @@ -0,0 +1,13 @@ +from fastapi import APIRouter, Query + +from app.core.response import success_response +from app.services.platform_service import platform_service + + +router = APIRouter(prefix="/alarm", tags=["alarm"]) + + +@router.get("/list") +def list_alarms(page: int = Query(default=1, ge=1), size: int = Query(default=20, ge=1, le=100)) -> dict: + data = platform_service.list_alarms(page=page, size=size) + return success_response(data, msg="获取报警历史成功") diff --git a/backend/app/api/routes/auth.py b/backend/app/api/routes/auth.py new file mode 100644 index 0000000..b98afa6 --- /dev/null +++ b/backend/app/api/routes/auth.py @@ -0,0 +1,16 @@ +from typing import Any, Dict + +from fastapi import APIRouter + +from app.core.response import success_response +from app.core.security import verify_access_password +from app.schemas.platform import PasswordVerifyIn + + +router = APIRouter(prefix="/auth", tags=["auth"]) + + +@router.post("/verify-password") +def verify_password_api(payload: PasswordVerifyIn) -> Dict[str, Any]: + is_valid = verify_access_password(payload.password) + return success_response(is_valid, msg="密码校验完成") diff --git a/backend/app/api/routes/config.py b/backend/app/api/routes/config.py new file mode 100644 index 0000000..410b0c6 --- /dev/null +++ b/backend/app/api/routes/config.py @@ -0,0 +1,36 @@ +from typing import Any, Dict, List + +from fastapi import APIRouter, Depends + +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.services.platform_service import platform_service + + +router = APIRouter(prefix="/config", tags=["config"]) + + +@router.post("/device", dependencies=[Depends(verify_api_token)]) +def save_device_config(payload: DeviceConfigIn) -> Dict[str, Any]: + return success_response(platform_service.save_device_config(payload)) + + +@router.post("/channel", dependencies=[Depends(verify_api_token)]) +def save_channel_config(payload: ChannelConfigIn) -> Dict[str, Any]: + return success_response(platform_service.save_channel_config(payload)) + + +@router.post("/line_alarm_setting", dependencies=[Depends(verify_api_token)]) +def save_line_alarm_setting(payload: LineAlarmSettingIn) -> Dict[str, Any]: + return success_response(platform_service.save_line_alarm_setting(payload)) + + +@router.post("/ai_alarm_setting", dependencies=[Depends(verify_api_token)]) +def save_ai_alarm_setting(payload: List[AiAlarmSettingIn]) -> Dict[str, Any]: + return success_response(platform_service.save_ai_alarm_setting(payload)) + + +@router.post("/system", dependencies=[Depends(verify_api_token)]) +def save_system_config(payload: SystemConfigIn) -> Dict[str, Any]: + return success_response(platform_service.save_system_config(payload)) diff --git a/backend/app/api/routes/control.py b/backend/app/api/routes/control.py new file mode 100644 index 0000000..2ef98bc --- /dev/null +++ b/backend/app/api/routes/control.py @@ -0,0 +1,14 @@ +from fastapi import APIRouter, Depends + +from app.core.response import success_response +from app.core.security import verify_api_token +from app.schemas.platform import SwitchControlIn +from app.services.platform_service import platform_service + + +router = APIRouter(prefix="/control", tags=["control"]) + + +@router.post("/switch", dependencies=[Depends(verify_api_token)]) +def switch_control(payload: SwitchControlIn) -> dict: + return success_response(platform_service.switch_control(payload)) diff --git a/backend/app/api/routes/realtime.py b/backend/app/api/routes/realtime.py new file mode 100644 index 0000000..f286152 --- /dev/null +++ b/backend/app/api/routes/realtime.py @@ -0,0 +1,13 @@ +from fastapi import APIRouter + +from app.core.response import success_response +from app.services.platform_service import platform_service + + +router = APIRouter(tags=["realtime"]) + + +@router.get("/real-time-data") +def get_realtime_data() -> dict: + data = platform_service.get_realtime_data() + return success_response(data.model_dump(), msg="获取实时数据成功") diff --git a/backend/app/api/routes/status.py b/backend/app/api/routes/status.py new file mode 100644 index 0000000..5de18c8 --- /dev/null +++ b/backend/app/api/routes/status.py @@ -0,0 +1,13 @@ +from fastapi import APIRouter + +from app.core.response import success_response +from app.services.platform_service import platform_service + + +router = APIRouter(tags=["status"]) + + +@router.get("/device-status") +def get_device_status() -> dict: + data = platform_service.get_device_status() + return success_response(data.model_dump(), msg="获取设备状态成功") diff --git a/backend/app/cache/memory_store.py b/backend/app/cache/memory_store.py new file mode 100644 index 0000000..33a290c --- /dev/null +++ b/backend/app/cache/memory_store.py @@ -0,0 +1,37 @@ +from collections import deque +from dataclasses import dataclass, field +from threading import Lock +from typing import Deque, Optional + +from app.schemas.platform import AlarmEvent, DeviceStatus, RealtimeData + + +@dataclass +class MemoryStore: + realtime_data: Optional[RealtimeData] = None + device_status: Optional[DeviceStatus] = None + recent_alarms: Deque[AlarmEvent] = field(default_factory=lambda: deque(maxlen=100)) + _lock: Lock = field(default_factory=Lock) + + def set_realtime(self, data: RealtimeData) -> None: + with self._lock: + self.realtime_data = data + + def get_realtime(self) -> Optional[RealtimeData]: + with self._lock: + return self.realtime_data + + def set_status(self, status: DeviceStatus) -> None: + with self._lock: + self.device_status = status + + def get_status(self) -> Optional[DeviceStatus]: + with self._lock: + return self.device_status + + def push_alarm(self, alarm: AlarmEvent) -> None: + with self._lock: + self.recent_alarms.appendleft(alarm) + + +memory_store = MemoryStore() diff --git a/backend/app/core/config.py b/backend/app/core/config.py new file mode 100644 index 0000000..89d30ae --- /dev/null +++ b/backend/app/core/config.py @@ -0,0 +1,31 @@ +from pathlib import Path + +from pydantic_settings import BaseSettings, SettingsConfigDict + + +BASE_DIR = Path(__file__).resolve().parents[2] + + +class Settings(BaseSettings): + app_name: str = "电气量测控平台" + app_version: str = "0.1.0" + api_prefix: str = "/api" + realtime_ws_path: str = "/ws/real-time" + alarm_ws_path: str = "/ws/alarm" + poll_interval_seconds: float = 0.5 + auth_password: str = "admin123" + use_mock_device: bool = True + config_dir: Path = BASE_DIR / "config" + data_dir: Path = BASE_DIR / "data" + alarm_db_path: Path = BASE_DIR / "data" / "alarm.db" + backup_dir: Path = BASE_DIR / "data" / "backups" + + model_config = SettingsConfigDict( + env_prefix="EMCP_", + env_file=".env", + env_file_encoding="utf-8", + extra="ignore", + ) + + +settings = Settings() diff --git a/backend/app/core/response.py b/backend/app/core/response.py new file mode 100644 index 0000000..6e2e484 --- /dev/null +++ b/backend/app/core/response.py @@ -0,0 +1,9 @@ +from typing import Any, Dict + + +def success_response(data: Any = None, msg: str = "操作成功", code: int = 200) -> Dict[str, Any]: + return { + "code": code, + "msg": msg, + "data": data if data is not None else {}, + } diff --git a/backend/app/core/security.py b/backend/app/core/security.py new file mode 100644 index 0000000..0152dd2 --- /dev/null +++ b/backend/app/core/security.py @@ -0,0 +1,27 @@ +import hashlib +import secrets +from typing import Optional + +from fastapi import Header, HTTPException, status + +from app.core.config import settings + + +def hash_password(password: str) -> str: + return hashlib.sha256(password.encode("utf-8")).hexdigest() + + +def verify_password(password: str, expected_hash: str) -> bool: + return hash_password(password) == expected_hash + + +def verify_access_password(password: str) -> bool: + return secrets.compare_digest(password, settings.auth_password) + + +def verify_api_token(x_api_token: Optional[str] = Header(default=None)) -> None: + if x_api_token != settings.auth_password: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="无效的访问令牌", + ) diff --git a/backend/app/db/sqlite.py b/backend/app/db/sqlite.py new file mode 100644 index 0000000..faf0b25 --- /dev/null +++ b/backend/app/db/sqlite.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +import sqlite3 +from pathlib import Path +from typing import Optional + +from app.core.config import settings + + +def get_connection(db_path: Optional[Path] = None) -> sqlite3.Connection: + path = db_path or settings.alarm_db_path + path.parent.mkdir(parents=True, exist_ok=True) + connection = sqlite3.connect(path) + connection.row_factory = sqlite3.Row + return connection + + +def init_db() -> None: + with get_connection() as connection: + connection.execute( + """ + CREATE TABLE IF NOT EXISTS alarm_event ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + alarm_type TEXT NOT NULL, + time TEXT NOT NULL, + no TEXT, + type TEXT, + content TEXT NOT NULL, + level TEXT NOT NULL + ) + """ + ) + connection.commit() diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..b108fff --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +import asyncio +from contextlib import asynccontextmanager + +from fastapi import FastAPI, WebSocket, WebSocketDisconnect +from fastapi.middleware.cors import CORSMiddleware + +from app.api.router import api_router +from app.core.config import settings +from app.db.sqlite import init_db +from app.tasks.polling import device_polling_loop, stop_task +from app.ws.manager import ws_manager + + +@asynccontextmanager +async def lifespan(_: FastAPI): + init_db() + poll_task = asyncio.create_task(device_polling_loop()) + try: + yield + finally: + await stop_task(poll_task) + + +app = FastAPI( + title=settings.app_name, + version=settings.app_version, + lifespan=lifespan, +) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(api_router, prefix=settings.api_prefix) + + +@app.get("/") +def healthcheck() -> dict: + return {"status": "ok", "service": settings.app_name} + + +@app.websocket(settings.realtime_ws_path) +async def realtime_ws(websocket: WebSocket) -> None: + await ws_manager.connect("real-time", websocket) + try: + while True: + await websocket.receive_text() + except WebSocketDisconnect: + ws_manager.disconnect("real-time", websocket) + + +@app.websocket(settings.alarm_ws_path) +async def alarm_ws(websocket: WebSocket) -> None: + await ws_manager.connect("alarm", websocket) + try: + while True: + await websocket.receive_text() + except WebSocketDisconnect: + ws_manager.disconnect("alarm", websocket) diff --git a/backend/app/repositories/alarm_repo.py b/backend/app/repositories/alarm_repo.py new file mode 100644 index 0000000..4fca949 --- /dev/null +++ b/backend/app/repositories/alarm_repo.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +from typing import Any, Dict, List + +from app.db.sqlite import get_connection +from app.schemas.platform import AlarmEvent + + +class AlarmRepository: + def save_alarm(self, alarm: AlarmEvent) -> int: + with get_connection() as connection: + cursor = connection.execute( + """ + INSERT INTO alarm_event (alarm_type, time, no, type, content, level) + VALUES (?, ?, ?, ?, ?, ?) + """, + ( + alarm.alarm_type, + alarm.time.isoformat(sep=" ", timespec="seconds"), + alarm.no, + alarm.type, + alarm.content, + alarm.level, + ), + ) + connection.commit() + return int(cursor.lastrowid) + + def list_alarms(self, page: int = 1, size: int = 20) -> List[Dict[str, Any]]: + offset = (page - 1) * size + with get_connection() as connection: + cursor = connection.execute( + """ + SELECT id, alarm_type, time, no, type, content, level + FROM alarm_event + ORDER BY id DESC + LIMIT ? OFFSET ? + """, + (size, offset), + ) + return [dict(row) for row in cursor.fetchall()] diff --git a/backend/app/repositories/json_config_repo.py b/backend/app/repositories/json_config_repo.py new file mode 100644 index 0000000..ebf37aa --- /dev/null +++ b/backend/app/repositories/json_config_repo.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any, Dict, List, Optional, Union + +from app.core.config import settings +from app.utils.backup import backup_file + + +class JsonConfigRepository: + def __init__(self, config_dir: Optional[Path] = None) -> None: + self.config_dir = config_dir or settings.config_dir + self.config_dir.mkdir(parents=True, exist_ok=True) + + def _write_json(self, filename: str, payload: Union[Dict[str, Any], List[Any]]) -> Path: + path = self.config_dir / filename + backup_file(path, settings.backup_dir) + temp_path = path.with_suffix(".tmp") + with temp_path.open("w", encoding="utf-8") as file: + json.dump(payload, file, ensure_ascii=False, indent=2) + temp_path.replace(path) + return path + + def write_device_config(self, payload: Dict[str, Any]) -> Path: + return self._write_json("device.json", payload) + + def write_channel_config(self, payload: Dict[str, Any]) -> Path: + return self._write_json("channel.json", payload) + + def write_setting_config(self, payload: Dict[str, Any]) -> Path: + return self._write_json("setting.json", payload) + + def read_json(self, filename: str) -> Union[Dict[str, Any], List[Any]]: + path = self.config_dir / filename + if not path.exists(): + return {} + with path.open("r", encoding="utf-8") as file: + return json.load(file) diff --git a/backend/app/schemas/common.py b/backend/app/schemas/common.py new file mode 100644 index 0000000..d6a3b22 --- /dev/null +++ b/backend/app/schemas/common.py @@ -0,0 +1,14 @@ +from typing import Any + +from pydantic import BaseModel, Field + + +class ApiResponse(BaseModel): + code: int = 200 + msg: str = "操作成功" + data: Any = Field(default_factory=dict) + + +class PaginationQuery(BaseModel): + page: int = Field(default=1, ge=1) + size: int = Field(default=20, ge=1, le=100) diff --git a/backend/app/schemas/platform.py b/backend/app/schemas/platform.py new file mode 100644 index 0000000..7cfb234 --- /dev/null +++ b/backend/app/schemas/platform.py @@ -0,0 +1,158 @@ +from datetime import datetime +from typing import Dict, List, Optional + +from pydantic import BaseModel, Field + + +class ValueGroup(BaseModel): + Ua: float = 0.0 + Ub: float = 0.0 + Uc: float = 0.0 + Ia: float = 0.0 + Ib: float = 0.0 + Ic: float = 0.0 + Pa: float = 0.0 + Pb: float = 0.0 + Pc: float = 0.0 + Pt: float = 0.0 + Qa: float = 0.0 + Qb: float = 0.0 + Qc: float = 0.0 + Qt: float = 0.0 + Sa: float = 0.0 + Sb: float = 0.0 + Sc: float = 0.0 + St: float = 0.0 + PFa: float = 0.0 + PFb: float = 0.0 + PFc: float = 0.0 + PFt: float = 0.0 + Uab: float = 0.0 + Ubc: float = 0.0 + Uca: float = 0.0 + frq: float = 50.0 + + +class LineData(BaseModel): + line_no: int + pri_val: ValueGroup = Field(default_factory=ValueGroup) + sec_val: ValueGroup = Field(default_factory=ValueGroup) + + +class RealtimeData(BaseModel): + line_list: List[LineData] + switch: Dict[str, int] + ai_collect: Dict[str, float] + + +class RealtimeEnvelope(BaseModel): + type: str = "real_time" + data: RealtimeData + + +class DeviceStatus(BaseModel): + self_check: str = "正常" + net1: str = "正常" + net2: str = "正常" + uart1: str = "正常" + uart2: str = "断开" + + +class HardwareVersion(BaseModel): + board_version: str = "B001.001.001" + display_version: str = "S001.001.001" + other_version: str = "Y001.001.001" + + +class SoftwareVersion(BaseModel): + display_program: str = "001.001.001" + communication_program: str = "001.001.001" + measurement_program: str = "001.001.001" + + +class NetConfigItem(BaseModel): + nic: str + ip: str = "" + mask: str = "" + gateway: str = "" + protocol: str = "" + + +class UartConfigItem(BaseModel): + port: str + baud: int = 9600 + parity: str = "NONE" + data_bits: int = 8 + stop_bits: int = 1 + protocol: str = "" + + +class DeviceConfigIn(BaseModel): + password: str = "123456" + hardware_version: HardwareVersion = Field(default_factory=HardwareVersion) + software_version: SoftwareVersion = Field(default_factory=SoftwareVersion) + net: List[NetConfigItem] = Field(default_factory=list) + uart: List[UartConfigItem] = Field(default_factory=list) + + +class ChannelItem(BaseModel): + ch: int + singal_type: str + line_no: int + type: str + limit_low: float + limit_high: float + + +class ChannelConfigIn(BaseModel): + ai_channel: List[ChannelItem] = Field(default_factory=list) + ao_channel: List[ChannelItem] = Field(default_factory=list) + + +class AlarmRule(BaseModel): + category: str + limit: Optional[float] = None + delay: int = 0 + output_node: str = "" + enabled: bool = True + + +class LineAlarmSettingIn(BaseModel): + line_no: int + over_limit_alarm: List[AlarmRule] = Field(default_factory=list) + fault_alarm: List[AlarmRule] = Field(default_factory=list) + + +class AiAlarmSettingIn(BaseModel): + channel_no: int + singal_type: str + limit_low: float + limit_high: float + delay: int = 0 + output_node: str = "" + enabled: bool = True + + +class SystemConfigIn(BaseModel): + time_sync: str = "auto" + brightness: int = Field(default=80, ge=0, le=100) + screen_saver: int = Field(default=60, ge=0) + + +class PasswordVerifyIn(BaseModel): + password: str + + +class AlarmEvent(BaseModel): + id: Optional[int] = None + alarm_type: str + time: datetime + no: str = "" + type: str = "" + content: str + level: str = "一般" + + +class SwitchControlIn(BaseModel): + ch: int = Field(ge=1) + action: int = Field(ge=0, le=1) diff --git a/backend/app/services/platform_service.py b/backend/app/services/platform_service.py new file mode 100644 index 0000000..44d8d8c --- /dev/null +++ b/backend/app/services/platform_service.py @@ -0,0 +1,105 @@ +from __future__ import annotations + +from typing import Any, Dict, List + +from app.adapters.device_client import CDeviceClient, MockDeviceClient +from app.cache.memory_store import memory_store +from app.core.config import settings +from app.core.security import hash_password +from app.repositories.alarm_repo import AlarmRepository +from app.repositories.json_config_repo import JsonConfigRepository +from app.schemas.platform import ( + AiAlarmSettingIn, + ChannelConfigIn, + DeviceConfigIn, + DeviceStatus, + LineAlarmSettingIn, + RealtimeData, + SwitchControlIn, + SystemConfigIn, +) +from app.ws.manager import ws_manager + + +class PlatformService: + def __init__(self) -> None: + self.config_repo = JsonConfigRepository() + self.alarm_repo = AlarmRepository() + self.device_client = MockDeviceClient() if settings.use_mock_device else CDeviceClient() + + def get_realtime_data(self) -> RealtimeData: + cached = memory_store.get_realtime() + if cached is not None: + return cached + realtime = self.device_client.read_realtime_data() + memory_store.set_realtime(realtime) + return realtime + + def get_device_status(self) -> DeviceStatus: + cached = memory_store.get_status() + if cached is not None: + return cached + status = self.device_client.read_device_status() + 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 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) + return {"save_path": f"/config/{path.name}", **device_result} + + 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 = {} + current["line_alarm_setting"] = payload.model_dump() + path = self.config_repo.write_setting_config(current) + device_result = self.device_client.send_line_alarm_setting(payload) + return {"save_path": f"/config/{path.name}", **device_result} + + def save_ai_alarm_setting(self, payload: List[AiAlarmSettingIn]) -> Dict[str, Any]: + current = self.config_repo.read_json("setting.json") + if not isinstance(current, dict): + 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 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) + + def list_alarms(self, page: int, size: int) -> List[Dict[str, Any]]: + return self.alarm_repo.list_alarms(page=page, size=size) + + def switch_control(self, payload: SwitchControlIn) -> Dict[str, Any]: + return self.device_client.send_switch_control(payload) + + async def poll_device_once(self) -> None: + realtime = self.device_client.read_realtime_data() + status = self.device_client.read_device_status() + alarms = self.device_client.read_alarm_events() + + memory_store.set_realtime(realtime) + memory_store.set_status(status) + await ws_manager.broadcast("real-time", {"type": "real_time", "data": realtime.model_dump()}) + + for alarm in alarms: + alarm.id = self.alarm_repo.save_alarm(alarm) + memory_store.push_alarm(alarm) + await ws_manager.broadcast("alarm", alarm.model_dump(mode="json")) + + +platform_service = PlatformService() diff --git a/backend/app/tasks/polling.py b/backend/app/tasks/polling.py new file mode 100644 index 0000000..4e79807 --- /dev/null +++ b/backend/app/tasks/polling.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +import asyncio +import contextlib +from typing import Optional + +from app.core.config import settings +from app.services.platform_service import platform_service + + +async def device_polling_loop() -> None: + while True: + await platform_service.poll_device_once() + await asyncio.sleep(settings.poll_interval_seconds) + + +async def stop_task(task: Optional[asyncio.Task]) -> None: + if task is None: + return + task.cancel() + with contextlib.suppress(asyncio.CancelledError): + await task diff --git a/backend/app/utils/backup.py b/backend/app/utils/backup.py new file mode 100644 index 0000000..d4a59a0 --- /dev/null +++ b/backend/app/utils/backup.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +import shutil +from datetime import datetime +from pathlib import Path + + +def backup_file(source: Path, backup_dir: Path) -> None: + if not source.exists(): + return + backup_dir.mkdir(parents=True, exist_ok=True) + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + target = backup_dir / f"{source.stem}_{timestamp}{source.suffix}" + shutil.copy2(source, target) diff --git a/backend/app/ws/manager.py b/backend/app/ws/manager.py new file mode 100644 index 0000000..a822d01 --- /dev/null +++ b/backend/app/ws/manager.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +import json +from collections import defaultdict +from typing import Any, DefaultDict, Dict, List, Set + +from fastapi import WebSocket + + +class WebSocketManager: + def __init__(self) -> None: + self._connections: DefaultDict[str, Set[WebSocket]] = defaultdict(set) + + async def connect(self, channel: str, websocket: WebSocket) -> None: + await websocket.accept() + self._connections[channel].add(websocket) + + def disconnect(self, channel: str, websocket: WebSocket) -> None: + self._connections[channel].discard(websocket) + + async def broadcast(self, channel: str, payload: Dict[str, Any]) -> None: + disconnected: List[WebSocket] = [] + for websocket in self._connections[channel]: + try: + await websocket.send_text(json.dumps(payload, ensure_ascii=False, default=str)) + except Exception: + disconnected.append(websocket) + for websocket in disconnected: + self.disconnect(channel, websocket) + + +ws_manager = WebSocketManager() diff --git a/backend/config/channel.json b/backend/config/channel.json new file mode 100644 index 0000000..0c863fe --- /dev/null +++ b/backend/config/channel.json @@ -0,0 +1,22 @@ +{ + "ai_channel": [ + { + "ch": 1, + "singal_type": "4-20mA22222", + "line_no": 1, + "type": "UA", + "limit_low": 0.0, + "limit_high": 20.0 + } + ], + "ao_channel": [ + { + "ch": 1, + "singal_type": "1-5v", + "line_no": 2, + "type": "UA", + "limit_low": 0.0, + "limit_high": 20.0 + } + ] +} \ No newline at end of file diff --git a/backend/config/device.json b/backend/config/device.json new file mode 100644 index 0000000..c501daf --- /dev/null +++ b/backend/config/device.json @@ -0,0 +1,47 @@ +{ + "password": "8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92", + "hardware_version": { + "board_version": "B001.001.002", + "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" + } + ] +} \ No newline at end of file diff --git a/backend/config/setting.json b/backend/config/setting.json new file mode 100644 index 0000000..e9c6f8b --- /dev/null +++ b/backend/config/setting.json @@ -0,0 +1,39 @@ +{ + "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 + } + ] + }, + "ai_alarm_setting": [ + { + "channel_no": 1, + "singal_type": "4-20mA", + "limit_low": 0.0, + "limit_high": 20.0, + "delay": 180, + "output_node": "开出1", + "enabled": true + } + ], + "system_config": { + "time_sync": "manual", + "brightness": 83, + "screen_saver": 60 + } +} \ No newline at end of file diff --git a/backend/pyproject.toml b/backend/pyproject.toml new file mode 100644 index 0000000..dafa401 --- /dev/null +++ b/backend/pyproject.toml @@ -0,0 +1,26 @@ +[project] +name = "emcp-backend" +version = "0.1.0" +description = "RK3568 电气量测控平台后端骨架" +readme = "README.md" +requires-python = ">=3.8" +dependencies = [ + "fastapi>=0.115,<1.0", + "uvicorn[standard]>=0.30,<1.0", + "pydantic>=2.7,<3.0", + "pydantic-settings>=2.3,<3.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.2,<9.0", + "httpx>=0.27,<1.0", +] + +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.pytest.ini_options] +pythonpath = ["."] +testpaths = ["tests"] diff --git a/backend/setup.py b/backend/setup.py new file mode 100644 index 0000000..4c50bfd --- /dev/null +++ b/backend/setup.py @@ -0,0 +1,21 @@ +from setuptools import find_packages, setup + + +setup( + name="emcp-backend", + version="0.1.0", + description="RK3568 电气量测控平台后端骨架", + packages=find_packages(), + install_requires=[ + "fastapi>=0.115,<1.0", + "uvicorn[standard]>=0.30,<1.0", + "pydantic>=2.7,<3.0", + "pydantic-settings>=2.3,<3.0", + ], + extras_require={ + "dev": [ + "pytest>=8.2,<9.0", + "httpx>=0.27,<1.0", + ] + }, +) diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py new file mode 100644 index 0000000..e8225cb --- /dev/null +++ b/backend/tests/test_api.py @@ -0,0 +1,20 @@ +from fastapi.testclient import TestClient + +from app.main import app + + +client = TestClient(app) + + +def test_healthcheck() -> None: + response = client.get("/") + assert response.status_code == 200 + assert response.json()["status"] == "ok" + + +def test_realtime_endpoint() -> None: + response = client.get("/api/real-time-data") + assert response.status_code == 200 + payload = response.json() + assert payload["code"] == 200 + assert "line_list" in payload["data"] diff --git a/document/gui.h b/document/gui.h new file mode 100644 index 0000000..ca9bc82 --- /dev/null +++ b/document/gui.h @@ -0,0 +1,207 @@ +#ifndef __GUI_H +#define __GUI_H + +#include "time.h" + +#define LINE_TOTAL_NUM 4 //· +#define ANALOG_CH_NUM 12 //AIAOģͨ + + +/**************************************************************С -> ****************************************************************************/ + +//ʵʱ +typedef struct +{ + //ѹ + float Ua; //Aѹ + float Ub; //Bѹ + float Uc; //Cѹ + + // + float Ia; //A + float Ib; //B + float Ic; //C + + //й + float Pa; + float Pb; + float Pc; + float Pt; //й + + // + float Qa; + float Qb; + float Qc; + float Qt; // + + //ڹ + float Sa; + float Sb; + float Sc; + float St; //ڹ + + // + float PFa; + float PFb; + float PFc; + float PFt; //ܹ + + //ߵѹ + float Uab; + float Ubc; + float Uca; + + //Ƶ + float frq; + +}measure_unit_t; + +//Bʱʱ +typedef union +{ + uint8_t b_code[6]; + + struct + { + uint8_t ascii_s_unit : 4; + uint8_t ascii_s_ten : 4; + uint8_t ascii_min_unit : 4; + uint8_t ascii_min_ten : 4; + uint8_t ascii_hour_unit : 4; + uint8_t ascii_hour_ten : 4; + uint8_t ascii_m_day_unit: 4; + uint8_t ascii_m_day_ten : 4; + uint8_t ascii_month_unit: 4; //4bit + uint8_t ascii_month_ten : 4; //4bit + uint8_t ascii_year_unit : 4; //4bit + uint8_t ascii_year_ten : 4; //4bit + }time; +}b_code_t; + + +//롢 +typedef struct +{ + uint16_t in; //ÿһλӦһͨ״̬ 1 = ϣ 0 = + uint16_t out; //ÿһλӦһͨ״̬ 1 = ϣ 0 = +}d_io_t; + +//ģ:AI AO +typedef struct +{ + float in[ANALOG_CH_NUM]; //ģ + float out[ANALOG_CH_NUM]; //ģ +}a_io_t; + +//¼ +typedef struct +{ + struct tm t; //¼ʱ + uint32_t ms; //¼ĺʱ + uint8_t evt; //ͣ0 = ¼ 1 = ¼ +}event_t; + +// +typedef struct +{ uint32_t stamp; //ʱ + int16_t ch[8]; //4·ʱ8ͨuaubuc, iaibicƵϵ + //AIɼͨʱ 8ͨӦ +}ch_data_t; + +// +typedef struct +{ + uint8_t line_index; //· 0 ~ 3 = 4·ѹݣ4 = AIɼ + ch_data_t dot[64]; //64һܲ +}waveform_data_t; + + +//С -> +typedef struct +{ + measure_unit_t measure_unit[LINE_TOTAL_NUM];//ܹ4· + + a_io_t d_io; //ģ롢 + + d_io_t a_io; //롢 + + b_code_t b_code; //Bʱ + + event_t event[2]; //¼־:event[0] = pt ߣ event[1] = CT + + waveform_data_t waveform; //ʵʱһܲ + +}gui_in_t; + + +/************************************************************** -> С****************************************************************************/ +//һģ·һ + + +//ڲ +typedef struct +{ + uint32_t baud_tate; // 9600 115200Ĭϣ + uint8_t parity; //Уλ 0 = У飬 1 = У飬 2 = żУ + uint8_t data; //λ 0 = 8λ + uint8_t stop; //ֹͣλ 0 = 1λ 1 = 2λ + uint8_t protocol; //ͨѶЭ 0 = Э飬1 = modbus +}com_param_t; + + +//ģֵͨ +typedef struct +{ + uint8_t sig_type;//ź 0 = 4~20maźţ 1 = 1~5Vź + uint8_t line_sub;//· 1 = ·1 2 = ·2 3 =·3 4 = ·4 0 = װ + uint8_t att; // : 0 = UA, 1 = UB, 2 = UC, 3 = UAB, 4 = UBC, 5 = UCA, 6 = P, 7 = Q, 8 = F + uint8_t u_limit; //ֵ : + uint8_t d_limit; //ֵ : +}analog_ch_param_t; + + +//·ֵ-> +typedef struct +{ + // uint8_t index; //·ţ 1 = ·1, 2 = ·2 3 = ·3, 4 = ·4 + uint8_t limit; //ֵ + uint8_t delay; //ʱ [1 ~ 10] + uint8_t out_num; //ڵ㣺 [1 = 1 2 = 2...] + uint8_t select; //Ͷˣ [0 = Ͷ 1 = Ͷ] +}line_param_t; + +typedef struct +{ + line_param_t type[7]; //7𣺵ѹ, ʣƵʣPTߣCT +}line_type_t; + + +//AI -> +typedef struct +{ + uint8_t u_limit; //ֵ : + uint8_t d_limit; //ֵ : + uint8_t delay; //ʱ [1 ~ 10] + uint8_t out_num; //ڵ㣺 [1 = 1 2 = 2...] + uint8_t select; //Ͷˣ [0 = Ͷ 1 = Ͷ] +}ai_alarm_param_t; + + +// -> С +//ģ +typedef struct +{ + struct tm t; //ʱ + + d_io_t a_io; //롢 + + com_param_t com_param[4]; //ڲ + line_type_t line_param[4]; //·ֵ: 4·ֵһ· + analog_ch_param_t a_in_param[12]; //12·AIģͨ + analog_ch_param_t a_out_param[12]; //12·AOģͨ + ai_alarm_param_t ai_alarm_param[12];//12·AIģ뱨 + +}gui_out_t; + + +#endif diff --git a/document/前后端启动说明.md b/document/前后端启动说明.md new file mode 100644 index 0000000..a167067 --- /dev/null +++ b/document/前后端启动说明.md @@ -0,0 +1,422 @@ +# 电气量测控平台前后端启动说明 + +## 1. 文档说明 + +本文档用于说明 `EMCP` 项目前后端的开发环境准备、依赖安装、启动步骤、访问地址以及常见问题处理方式。 + +项目目录: + +- 根目录:`d:\Trae_space\EMCP` +- 后端目录:`d:\Trae_space\EMCP\backend` +- 前端目录:`d:\Trae_space\EMCP\frontend` +- 文档目录:`d:\Trae_space\EMCP\document` + +--- + +## 2. 当前项目技术栈 + +### 2.1 后端 + +- 语言:`Python 3.8` +- 当前虚拟环境版本:`Python 3.8.10` +- 框架:`FastAPI` +- 服务启动:`Uvicorn` +- 配置存储:`JSON` +- 报警存储:`SQLite` +- 实时通信:`WebSocket` + +### 2.2 前端 + +- 框架:`Vue 3` +- 构建工具:`Vite` +- HTTP 请求:`Axios` + +--- + +## 3. 启动前准备 + +### 3.1 后端准备 + +后端目录已创建虚拟环境: + +- 虚拟环境路径:`d:\Trae_space\EMCP\backend\.venv` + +如虚拟环境不存在,可在 `backend` 目录重新创建: + +```powershell +cd d:\Trae_space\EMCP\backend +py -3.8 -m venv .venv +``` + +激活虚拟环境: + +```powershell +cd d:\Trae_space\EMCP\backend +.\.venv\Scripts\Activate.ps1 +``` + +### 3.2 前端准备 + +确保本机已安装: + +- `Node.js` +- `npm` + +进入前端目录后通过 `npm` 安装依赖。 + +--- + +## 4. 首次安装依赖 + +### 4.1 安装后端依赖 + +```powershell +cd d:\Trae_space\EMCP\backend +.\.venv\Scripts\Activate.ps1 +pip install -e .[dev] +``` + +说明: + +- 当前项目已包含 `setup.py` 与 `pyproject.toml` +- 若 `pip install -e .[dev]` 因环境兼容问题失败,可先升级 `pip` 后重试: + +```powershell +python -m pip install --upgrade pip +pip install -e .[dev] +``` + +### 4.2 安装前端依赖 + +```powershell +cd d:\Trae_space\EMCP\frontend +npm install +``` + +--- + +## 5. 启动后端 + +### 5.1 启动命令 + +```powershell +cd d:\Trae_space\EMCP\backend +.\.venv\Scripts\Activate.ps1 +.\.venv\Scripts\python.exe -m uvicorn app.main:app --reload +``` + +### 5.2 启动成功标志 + +终端出现类似信息表示启动成功: + +```text +Uvicorn running on http://127.0.0.1:8000 +``` + +### 5.3 后端访问地址 + +- 健康检查:`http://127.0.0.1:8000/` +- 接口文档:`http://127.0.0.1:8000/docs` +- OpenAPI:`http://127.0.0.1:8000/openapi.json` + +### 5.4 后端主要接口 + +- 实时数据:`GET /api/real-time-data` +- 设备状态:`GET /api/device-status` +- 设备配置:`POST /api/config/device` +- 通道配置:`POST /api/config/channel` +- 线路报警设置:`POST /api/config/line_alarm_setting` +- AI 报警设置:`POST /api/config/ai_alarm_setting` +- 系统设置:`POST /api/config/system` +- 报警历史:`GET /api/alarm/list` +- 控制指令:`POST /api/control/switch` + +### 5.5 WebSocket 地址 + +- 实时数据:`ws://127.0.0.1:8000/ws/real-time` +- 报警推送:`ws://127.0.0.1:8000/ws/alarm` + +### 5.6 后端 Debug 调试启动 + +开发阶段如果需要断点调试后端,推荐使用以下两种方式。 + +#### 方式一:在 IDE 中直接调试 `uvicorn` + +适用于 `Trae / VS Code / Cursor` 一类支持 `launch.json` 的编辑器。 + +可在工作区创建或补充调试配置文件: + +- `d:\Trae_space\EMCP\.vscode\launch.json` + +示例配置: + +```json +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug FastAPI Backend", + "type": "python", + "request": "launch", + "module": "uvicorn", + "cwd": "d:\\Trae_space\\EMCP\\backend", + "args": [ + "app.main:app", + "--host", + "127.0.0.1", + "--port", + "8000", + "--reload" + ], + "jinja": true, + "justMyCode": true + } + ] +} +``` + +使用方式: + +1. 打开项目根目录 `d:\Trae_space\EMCP` +2. 选择 `Debug FastAPI Backend` +3. 启动调试 +4. 在 `backend/app/` 下设置断点 + +说明: + +- `cwd` 必须指向 `backend`,否则 `app.main:app` 可能导入失败 +- 调试时建议使用后端虚拟环境解释器:`d:\Trae_space\EMCP\backend\.venv\Scripts\python.exe` +- 如果 `--reload` 导致断点命中不稳定,可临时去掉 `--reload` + +#### 方式二:使用 `debugpy` 启动后再附加调试 + +先安装调试依赖: + +```powershell +cd d:\Trae_space\EMCP\backend +.\.venv\Scripts\Activate.ps1 +pip install debugpy +``` + +然后使用以下命令启动: + +```powershell +cd d:\Trae_space\EMCP\backend +.\.venv\Scripts\Activate.ps1 +python -m debugpy --listen 5678 -m uvicorn app.main:app --host 127.0.0.1 --port 8000 --reload +``` + +启动后可在 IDE 中使用“附加到进程/Attach”方式连接: + +- 主机:`127.0.0.1` +- 端口:`5678` + +如果不需要热重载,也可以使用更稳定的命令: + +```powershell +cd d:\Trae_space\EMCP\backend +.\.venv\Scripts\Activate.ps1 +python -m debugpy --listen 5678 -m uvicorn app.main:app --host 127.0.0.1 --port 8000 +``` + +#### Debug 调试建议 + +- 需要稳定断点时,优先关闭 `--reload` +- 排查接口逻辑时,建议在以下位置打断点: + - `backend/app/api/routes/` + - `backend/app/services/platform_service.py` + - `backend/app/adapters/device_client.py` +- 如果要调试模拟采集链路,可重点查看: + - `backend/app/tasks/polling.py` + - `backend/app/cache/memory_store.py` + - `backend/app/ws/manager.py` + +--- + +## 6. 启动前端 + +### 6.1 启动命令 + +```powershell +cd d:\Trae_space\EMCP\frontend +npm run dev -- --host 127.0.0.1 --port 5173 +``` + +### 6.2 启动成功标志 + +终端出现类似地址表示启动成功: + +```text +Local: http://127.0.0.1:5173/ +``` + +### 6.3 前端访问地址 + +- 前端页面:`http://127.0.0.1:5173/` + +--- + +## 7. 推荐启动顺序 + +建议按以下顺序启动: + +1. 先启动后端 +2. 再启动前端 +3. 浏览器打开前端地址进行联调 + +原因: + +- 前端默认请求后端接口:`http://127.0.0.1:8000/api` +- 前端默认连接后端 WebSocket: + - `ws://127.0.0.1:8000/ws/real-time` + - `ws://127.0.0.1:8000/ws/alarm` + +前端接口配置位置: + +- `frontend/src/api/http.ts` + +默认请求头中已携带: + +- `X-API-Token: admin123` + +--- + +## 8. 模拟系统说明 + +当前项目后端默认启用模拟设备采集。 + +后端配置位置: + +- `backend/app/core/config.py` + +当前关键配置: + +```python +use_mock_device: bool = True +poll_interval_seconds: float = 0.5 +``` + +说明: + +- `use_mock_device = True` 表示使用模拟设备数据源 +- 后端会按 `0.5` 秒周期采集模拟数据 +- 前端可用于联调实时数据、设备状态、报警历史、控制指令、配置保存等功能 + +--- + +## 9. 当前已打通页面 + +当前前端页面均已可以进入,已完成联调的页面如下: + +- 实时数据 +- 设备状态 +- 设备配置 +- 通道配置 +- 报警设置 +- 报警历史 +- 控制指令 +- 系统设置 + +其中: + +- `设备状态` 支持手动刷新 +- `报警历史` 支持分页和刷新 +- `设备配置 / 通道配置 / 报警设置` 已接通真实后端提交接口 + +--- + +## 10. 常用开发命令 + +### 10.1 后端测试 + +```powershell +cd d:\Trae_space\EMCP\backend +.\.venv\Scripts\Activate.ps1 +python -m pytest +``` + +### 10.2 前端构建 + +```powershell +cd d:\Trae_space\EMCP\frontend +npm run build +``` + +--- + +## 11. 常见问题 + +### 11.1 后端启动失败,提示类型注解错误 + +如果出现类似报错: + +```text +TypeError: 'type' object is not subscriptable +``` + +原因通常是 Python 版本不兼容。当前项目要求使用: + +- `Python 3.8` + +请确认虚拟环境版本是否正确。 + +### 11.2 前端页面打不开 + +请检查: + +- 前端服务是否已启动 +- 地址是否为 `http://127.0.0.1:5173/` +- `npm install` 是否已执行成功 + +### 11.3 前端有页面但接口不通 + +请检查: + +- 后端服务是否已启动 +- 后端地址是否为 `http://127.0.0.1:8000` +- `frontend/src/api/http.ts` 中 `baseURL` 是否正确 + +### 11.4 页面能打开但实时数据不更新 + +请检查: + +- 后端是否正常运行 +- 后端轮询任务是否已启动 +- WebSocket 是否可连 +- 若 WebSocket 未连通,前端当前会自动回退到 HTTP 轮询读取实时数据 + +### 11.5 保存接口返回 401 或 403 + +请检查请求头中是否包含: + +```text +X-API-Token: admin123 +``` + +--- + +## 12. 关闭服务 + +在各自终端中按: + +```text +Ctrl + C +``` + +即可停止前端或后端服务。 + +--- + +## 13. 建议 + +日常开发建议保持两个独立终端: + +- 终端 1:后端 `uvicorn` +- 终端 2:前端 `vite` + +如果需要后续交付或部署,可在本说明基础上继续补充: + +- 生产环境启动说明 +- Windows 开机自启说明 +- Ubuntu / RK3568 部署说明 +- Nginx 反向代理说明 diff --git a/document/在RK3568设备上系统部署及使用说明.md b/document/在RK3568设备上系统部署及使用说明.md new file mode 100644 index 0000000..024934b --- /dev/null +++ b/document/在RK3568设备上系统部署及使用说明.md @@ -0,0 +1,579 @@ +# RK3568 设备上系统部署及使用说明 + +## 1. 文档目的 + +本文档用于说明如何在 `RK3568 + Ubuntu 20.04` 设备上部署 `EMCP` 电气量测控平台的前后端系统,并完成以下目标: + +- 部署后端 `FastAPI + Uvicorn` 服务 +- 部署前端静态页面 +- 配置前后端开机自动启动 +- 登录桌面后自动打开浏览器并访问主页面 +- 给出现场快速设置和日常使用说明 + +--- + +## 2. 适用范围 + +适用对象: + +- 硬件平台:`RK3568` +- 操作系统:`Ubuntu 20.04` +- 部署模式:前后端均部署在同一台设备本机 + +当前工程实际情况: + +- 后端运行端口:`8000` +- 前端访问地址建议使用:`http://127.0.0.1:5173/` +- 前端默认请求后端地址:`http://127.0.0.1:8000/api` +- 前端默认请求头中携带:`X-API-Token: admin123` +- 默认安全校验密码:`admin123` +- 后端默认启用模拟采集:`use_mock_device = True` + +说明: + +- 当前前端代码默认调用本机 `127.0.0.1:8000` 的后端接口,因此推荐前后端部署在同一台 RK3568 设备上 +- 如果后续需要将前后端拆分到不同设备,需要同步调整前端接口配置 + +--- + +## 3. 部署建议 + +推荐目录规划如下: + +```text +/opt/emcp/ +├── backend/ +├── frontend/ +└── scripts/ +``` + +建议使用以下方式部署: + +- 后端:`systemd` 托管 `uvicorn` +- 前端:前端执行 `npm run build` 后,用轻量静态服务对 `dist/` 目录发布 +- 浏览器自启动:使用桌面自动启动项 `~/.config/autostart/*.desktop` + +--- + +## 4. 环境准备 + +### 4.1 安装系统依赖 + +在 RK3568 设备终端执行: + +```bash +sudo apt update +sudo apt install -y python3.8 python3.8-venv python3-pip curl git +sudo apt install -y nodejs npm +``` + +说明: + +- Ubuntu 20.04 默认可直接使用 `Python 3.8` +- 如果设备上的 `nodejs` 版本过低,建议升级到 `Node.js 18 LTS` 后再安装前端依赖 + +### 4.2 安装浏览器 + +如果设备带图形桌面,建议至少安装一个浏览器: + +```bash +sudo apt install -y chromium-browser +``` + +如果系统仓库没有 `chromium-browser`,也可以使用: + +```bash +sudo apt install -y firefox +``` + +### 4.3 拷贝项目代码 + +将工程目录拷贝到设备,例如: + +```bash +sudo mkdir -p /opt/emcp +sudo chown -R $USER:$USER /opt/emcp +``` + +然后将项目复制到: + +```text +/opt/emcp/backend +/opt/emcp/frontend +``` + +如果是通过 `git` 拉取,也可以在 `/opt/emcp` 下执行: + +```bash +git clone <你的仓库地址> /opt/emcp +``` + +如果项目目录本身已经包含 `backend` 和 `frontend`,则以实际目录结构为准。 + +--- + +## 5. 后端部署 + +### 5.1 创建虚拟环境 + +```bash +cd /opt/emcp/backend +python3.8 -m venv .venv +source .venv/bin/activate +python -m pip install --upgrade pip +``` + +### 5.2 安装后端依赖 + +```bash +cd /opt/emcp/backend +source .venv/bin/activate +pip install -e . +``` + +如需测试或调试工具,再安装: + +```bash +pip install -e .[dev] +``` + +### 5.3 手动启动后端验证 + +```bash +cd /opt/emcp/backend +source .venv/bin/activate +python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 +``` + +验证方式: + +- 本机健康检查:`http://127.0.0.1:8000/` +- 接口文档:`http://127.0.0.1:8000/docs` + +如果能正常返回服务信息,说明后端部署成功。 + +--- + +## 6. 前端部署 + +### 6.1 安装前端依赖 + +```bash +cd /opt/emcp/frontend +npm install +``` + +### 6.2 构建前端 + +```bash +cd /opt/emcp/frontend +npm run build +``` + +构建完成后会生成: + +```text +/opt/emcp/frontend/dist +``` + +### 6.3 手动启动前端验证 + +推荐使用 Python 内置静态服务快速发布: + +```bash +cd /opt/emcp/frontend +python3 -m http.server 5173 --directory dist --bind 0.0.0.0 +``` + +验证地址: + +- 前端页面:`http://127.0.0.1:5173/` + +说明: + +- 当前前端默认调用本机后端 `http://127.0.0.1:8000/api` +- 因此前端页面只要在设备本机打开,即可直接访问后端服务 + +--- + +## 7. 首次联调检查 + +建议按以下顺序进行: + +1. 先启动后端 +2. 再启动前端静态服务 +3. 浏览器打开 `http://127.0.0.1:5173/` +4. 检查实时数据、设备状态、报警历史等页面是否能正常进入 + +当前项目已打通页面包括: + +- 实时数据 +- 设备状态 +- 设备配置 +- 通道配置 +- 报警设置 +- 报警历史 +- 控制指令 +- 系统设置 + +补充说明: + +- `设备状态` 页面支持刷新 +- `报警历史` 页面支持分页和刷新 +- 配置保存前会弹出安全校验密码输入框 +- 当前默认安全校验密码为 `admin123` + +--- + +## 8. 配置开机自动启动 + +推荐使用 `systemd` 管理前后端服务。 + +### 8.1 配置后端服务 + +创建文件: + +```text +/etc/systemd/system/emcp-backend.service +``` + +内容如下: + +```ini +[Unit] +Description=EMCP Backend Service +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=ubuntu +WorkingDirectory=/opt/emcp/backend +ExecStart=/opt/emcp/backend/.venv/bin/python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target +``` + +说明: + +- `User=ubuntu` 需要替换为设备实际登录用户名 +- 如需切换真实采集设备,可结合 `.env` 或配置文件调整后端参数 + +### 8.2 配置前端服务 + +创建文件: + +```text +/etc/systemd/system/emcp-frontend.service +``` + +内容如下: + +```ini +[Unit] +Description=EMCP Frontend Static Service +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=ubuntu +WorkingDirectory=/opt/emcp/frontend +ExecStart=/usr/bin/python3 -m http.server 5173 --directory /opt/emcp/frontend/dist --bind 0.0.0.0 +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target +``` + +### 8.3 启用开机自启 + +执行: + +```bash +sudo systemctl daemon-reload +sudo systemctl enable emcp-backend.service +sudo systemctl enable emcp-frontend.service +sudo systemctl start emcp-backend.service +sudo systemctl start emcp-frontend.service +``` + +查看状态: + +```bash +sudo systemctl status emcp-backend.service +sudo systemctl status emcp-frontend.service +``` + +查看日志: + +```bash +journalctl -u emcp-backend.service -f +journalctl -u emcp-frontend.service -f +``` + +--- + +## 9. 开机自动打开浏览器访问主页面 + +本功能依赖图形桌面环境。若设备只启动命令行、不进入桌面,则无法自动弹出浏览器。 + +### 9.1 前置条件 + +建议提前确认以下两项: + +- 设备已安装浏览器 +- 设备已启用桌面自动登录,或现场人员上电后会手动登录桌面 + +说明: + +- 只有在用户登录图形桌面后,自动启动项才会执行 +- 如果希望“上电后无需人工登录就自动打开页面”,需要同时开启系统自动登录 + +### 9.2 创建浏览器启动脚本 + +创建目录: + +```bash +mkdir -p /opt/emcp/scripts +``` + +创建脚本文件: + +```text +/opt/emcp/scripts/start-emcp-browser.sh +``` + +内容如下: + +```bash +#!/bin/bash +sleep 8 + +URL="http://127.0.0.1:5173/" + +if command -v chromium-browser >/dev/null 2>&1; then + chromium-browser --start-fullscreen "$URL" >/dev/null 2>&1 & +elif command -v firefox >/dev/null 2>&1; then + firefox "$URL" >/dev/null 2>&1 & +else + xdg-open "$URL" >/dev/null 2>&1 & +fi +``` + +赋予执行权限: + +```bash +chmod +x /opt/emcp/scripts/start-emcp-browser.sh +``` + +说明: + +- `sleep 8` 用于等待前后端服务启动完成 +- 如果设备性能较低,可改为 `sleep 10` 或 `sleep 15` +- 如需全屏显示,优先使用 `chromium-browser --start-fullscreen` + +### 9.3 配置桌面自动启动项 + +创建目录: + +```bash +mkdir -p ~/.config/autostart +``` + +创建文件: + +```text +~/.config/autostart/emcp-browser.desktop +``` + +内容如下: + +```ini +[Desktop Entry] +Type=Application +Name=EMCP Browser Autostart +Exec=/opt/emcp/scripts/start-emcp-browser.sh +X-GNOME-Autostart-enabled=true +Terminal=false +``` + +配置完成后,用户每次登录桌面时都会自动打开浏览器并访问: + +```text +http://127.0.0.1:5173/ +``` + +--- + +## 10. 快捷设置说明 + +这一节适合现场交付时快速操作。 + +### 10.1 一次性完成部署 + +按以下顺序操作即可: + +1. 拷贝项目到 `/opt/emcp` +2. 创建后端虚拟环境并安装依赖 +3. 前端执行 `npm install` 和 `npm run build` +4. 创建两个 `systemd` 服务 +5. 创建浏览器自动启动脚本 +6. 创建 `~/.config/autostart/emcp-browser.desktop` +7. 执行 `systemctl enable` 完成开机自启 + +### 10.2 最常用检查命令 + +检查后端状态: + +```bash +sudo systemctl status emcp-backend.service +``` + +检查前端状态: + +```bash +sudo systemctl status emcp-frontend.service +``` + +重启后端: + +```bash +sudo systemctl restart emcp-backend.service +``` + +重启前端: + +```bash +sudo systemctl restart emcp-frontend.service +``` + +重新打开浏览器主页: + +```bash +/opt/emcp/scripts/start-emcp-browser.sh +``` + +### 10.3 修改主页地址 + +如果前端端口改为其他端口,例如 `8080`,需要同步修改以下两处: + +- `/opt/emcp/scripts/start-emcp-browser.sh` 中的 `URL` +- 前端静态服务端口或访问地址 + +### 10.4 修改登录后自动打开行为 + +如果不希望每次登录都自动打开浏览器,可以删除或重命名: + +```text +~/.config/autostart/emcp-browser.desktop +``` + +--- + +## 11. 日常使用说明 + +### 11.1 启动后访问方式 + +当设备开机完成并进入桌面后,系统会自动: + +- 启动后端服务 +- 启动前端静态服务 +- 打开浏览器访问主页 + +默认主页地址: + +```text +http://127.0.0.1:5173/ +``` + +### 11.2 页面使用提示 + +- 实时数据页:查看模拟或采集到的实时数据 +- 设备状态页:支持手动刷新状态 +- 报警历史页:支持分页和刷新 +- 配置类页面:保存前需要输入安全校验密码 + +当前默认安全校验密码: + +```text +admin123 +``` + +建议交付后尽快修改为现场正式密码。 + +--- + +## 12. 常见问题 + +### 12.1 浏览器没有自动打开 + +请检查: + +- 是否进入了图形桌面 +- `~/.config/autostart/emcp-browser.desktop` 是否存在 +- `/opt/emcp/scripts/start-emcp-browser.sh` 是否有执行权限 +- 浏览器是否已安装 + +### 12.2 浏览器打开了,但页面访问失败 + +请检查: + +- 前端服务是否启动成功 +- `5173` 端口是否被其他程序占用 +- `/opt/emcp/frontend/dist` 是否已经构建生成 + +### 12.3 页面能打开,但接口不通 + +请检查: + +- 后端服务是否启动成功 +- `8000` 端口是否正常监听 +- 前端默认接口地址是否仍为 `http://127.0.0.1:8000/api` + +### 12.4 页面保存时报权限或校验失败 + +请检查: + +- 默认请求头 `X-API-Token` 是否被修改 +- 输入的安全校验密码是否正确 +- 当前默认密码是否仍为 `admin123` + +### 12.5 修改代码后页面没有变化 + +请检查是否重新执行了前端构建: + +```bash +cd /opt/emcp/frontend +npm run build +sudo systemctl restart emcp-frontend.service +``` + +后端代码修改后则需要重启后端服务: + +```bash +sudo systemctl restart emcp-backend.service +``` + +--- + +## 13. 推荐交付方式 + +建议现场交付时采用以下方式: + +- 设备开机自动进入桌面 +- 后端和前端均使用 `systemd` 自启动 +- 浏览器自动全屏打开主页 +- 保留一个终端窗口或 SSH 方式用于维护 + +如需进一步增强生产部署能力,后续可继续补充: + +- `Nginx` 托管前端静态资源 +- 反向代理后端接口 +- HTTPS 证书 +- 真实硬件采集驱动接入 +- 自动备份配置与报警数据库 diff --git a/document/在RK3568设备上系统部署及使用说明.pdf b/document/在RK3568设备上系统部署及使用说明.pdf new file mode 100644 index 0000000..b5b5199 Binary files /dev/null and b/document/在RK3568设备上系统部署及使用说明.pdf differ diff --git a/document/电气量测控平台-显示界面设计和数据接口说.docx b/document/电气量测控平台-显示界面设计和数据接口说.docx new file mode 100644 index 0000000..339d11c Binary files /dev/null and b/document/电气量测控平台-显示界面设计和数据接口说.docx differ diff --git a/document/电气量测控平台-显示界面设计和数据接口说.pdf b/document/电气量测控平台-显示界面设计和数据接口说.pdf new file mode 100644 index 0000000..349a528 Binary files /dev/null and b/document/电气量测控平台-显示界面设计和数据接口说.pdf differ diff --git a/document/电气量测控平台系统需求分析及技术框架设计.md b/document/电气量测控平台系统需求分析及技术框架设计.md new file mode 100644 index 0000000..a437fbb --- /dev/null +++ b/document/电气量测控平台系统需求分析及技术框架设计.md @@ -0,0 +1,689 @@ +# 电气量测控平台系统需求分析与架构设计技术方案 +**适配平台:RK3568 嵌入式平台(ubuntu 22.04系统)** +**技术栈:FastAPI + HTTP + WebSocket + Python + Web前端** + +## 一、文档概述 +### 1.1 文档目的 +本文档基于《电气量测控平台显示界面设计和数据接口说明》、`gui.h`头文件及项目功能描述,明确**RK3568嵌入式平台**上Python编写、Web界面展示的电气量测控配置软件的功能、性能、接口、数据存储需求,完成系统架构设计,为软件开发、测试及交付提供技术依据。 + +### 1.2 适用范围 +本文档适用于**RK3568嵌入式硬件**,以Python为开发语言、FastAPI为服务框架、Web为交互界面的电气量测控配置软件,涵盖参数配置、实时数据交互、状态监测、报警管理等全流程功能设计。 + +### 1.3 参考资料 +1. 《电气量测控平台显示界面设计和数据接口说明》 +2. `gui.h`嵌入式程序头文件 +3. 项目功能需求描述 + +## 二、系统需求分析 +### 2.1 运行环境需求 +- **硬件环境**:RK3568嵌入式开发板(支持Linux系统、文件系统、SQLite数据库) +- **软件环境**:Linux操作系统、Python 3.8、**FastAPI**、Uvicorn ASGI服务、SQLite数据库、JSON文件存储支持 +- **交互环境**:Web浏览器(支持WebSocket、动态数据渲染、按钮交互、表格/波形展示) + +### 2.2 功能需求 +#### 2.2.1 参数配置管理功能 +实现测控设备全维度参数配置,配置项支持**JSON文件本地存储**,关键参数通过接口下发至C语言嵌入式程序,具体配置项如下: +1. **基础设备配置** + - 设备软件版本信息展示与配置 + - 通讯参数设置:串口、网口参数配置 + - 操作密码设置:支持密码修改、本地JSON文件存储 +2. **装置通道配置** + - AI通道设置:模拟量输入配置、信号类型、所属线路、数据阈值 + - AO通道设置:模拟量输出配置 + - 定值设置:电压/电流/功率等超限限值、检测延时、报警输出配置 +3. **系统管理配置** + - 对时设置:参数实时下发嵌入式程序 + - 灯光设置:屏幕亮度、屏保时间,参数实时下发嵌入式程序 + +#### 2.2.2 实时数据动态显示功能 +1. **数据读取周期**:间隔**0.5秒**从C语言控制系统读取实时数据 +2. **数据类型**:模拟量、开关量、AI采集数据 +3. **推送方式**:**WebSocket主动推送**至Web界面,动态刷新展示 +4. **存储规则**:动态数据仅内存缓存,**不保存到本地** + +#### 2.2.3 设备状态实时监测功能 +从嵌入式系统动态读取设备状态,Web界面实时展示: +- 装置自检状态(正常/异常) +- 网络1状态、网络2状态 +- 串口1状态、串口2状态 + +#### 2.2.4 报警事件管理功能 +1. **报警数据采集**:实时获取嵌入式设备报警事件 +2. **数据存储**:报警事件保存至**SQLite数据库** +3. **展示方式**:**WebSocket实时推送**报警至Web界面,支持弹窗提醒+历史查询 + +### 2.3 数据交互需求 +#### 2.3.1 上行数据(嵌入式系统→Python服务) +- 实时数据:0.5秒/次,模拟量、开关量、AI采集数据 +- 状态数据:装置自检、网络、串口状态 +- 报警数据:实时报警事件数据 + +#### 2.3.2 下行数据(Python服务→嵌入式系统) +- 配置数据:设备参数、通道配置、定值、对时、灯光参数 +- 控制指令:开关量分/合控制信号 + +### 2.4 数据存储需求 +1. **JSON文件存储**:设备版本、通讯参数、密码、AI/AO配置、定值配置等**非实时参数** +2. **SQLite数据库**:报警事件数据(支持历史查询) +3. **动态数据**:仅内存缓存,不持久化存储 + +### 2.5 性能需求 +1. **数据刷新率**:实时数据读取与WebSocket推送间隔≤0.5秒 +2. **响应速度**:参数配置下发、开关控制指令响应时间≤1秒 +3. **稳定性**:7×24小时连续运行 +4. **兼容性**:适配RK3568硬件,兼容嵌入式C程序接口 + +### 2.6 通信服务需求 +1. **HTTP服务**:基于FastAPI提供**RESTful API**,用于参数配置、状态查询、报警查询、控制指令 +2. **WebSocket服务**:基于FastAPI提供双工通信,用于**实时数据、报警数据主动推送**至Web界面 + +## 三、系统架构设计 +### 3.1 总体架构 +系统采用**分层架构 + 前后端分离**设计,从下到上分为: +**硬件层 → 嵌入式C程序层 → Python FastAPI服务层 → Web界面层** + +``` +Web界面层(HTML/CSS/JS) + ↓↑(HTTP + WebSocket) +Python FastAPI服务层(RESTful接口 + WebSocket推送 + 数据处理) + ↓↑(内部进程/核间通讯) +嵌入式C程序层(数据采集 + 控制执行) + ↓ +硬件层(RK3568 + 测控硬件 + 通讯模块) +``` + +### 3.2 分层详细设计 +#### 3.2.1 硬件层 +- 核心硬件:RK3568嵌入式开发板 +- 外设模块:AI/AO采集模块、开关量模块、网络/串口模块、显示屏 + +#### 3.2.2 嵌入式C程序层 +- 数据采集模块:20ms周期采集模拟量、开关量、AI数据 +- 控制执行模块:接收参数与指令,执行配置、对时、灯光、开关控制 +- 状态监测模块:自检、网口、串口状态实时上报 +- 报警生成模块:异常事件生成并上传 +- 通讯模块:与Python服务双向数据交互 + +#### 3.2.3 Python FastAPI服务层(核心) +采用**FastAPI + Uvicorn**搭建,提供**HTTP接口**和**WebSocket推送**双服务: +1. **RESTful API模块**:处理配置提交、状态查询、报警查询、控制指令 +2. **WebSocket推送模块**:0.5秒推送实时数据、实时报警数据到Web端 +3. **参数配置模块**:JSON文件读写、配置下发C程序 +4. **实时数据模块**:定时读取C程序数据,缓存并通过WebSocket推送 +5. **报警管理模块**:接收报警→存储SQLite→WebSocket推送前端 +6. **设备状态模块**:采集并缓存设备运行状态 + +#### 3.2.4 Web界面层 +- 菜单区:配置、状态、数据、报警、控制入口 +- 状态区:自检、网络、串口实时状态 +- 主显示区:实时数据表格、配置表单、控制按钮、报警列表 +- 报警区:WebSocket实时弹窗+滚动显示 + +### 3.3 数据流向设计 +1. **实时/报警数据流向** +嵌入式C程序 → Python → WebSocket主动推送 → Web界面(0.5秒刷新) +2. **参数配置流向** +Web界面 → HTTP POST → Python → JSON存储 + 下发C程序 +3. **状态/查询流向** +Web界面 → HTTP GET → Python → 返回数据展示 + +## 四、接口详细设计(RESTful + WebSocket) +### 4.1 RESTful API 接口详情(完整参数+返回值) +所有接口统一返回格式: +```json +{ + "code": 200, // 200=成功,400=参数错误,500=服务异常 + "msg": "操作成功", + "data": {} // 业务数据 +} +``` + +--- + +#### 1. GET /api/real-time-data +**作用**:获取最新一次实时数据(备用,主用WebSocket) +**参数**:无 +**返回data**: + +```json +{ + "code": 200, + "msg": "获取实时数据成功", + "data": { + "line_list": [ + { + "line_no": 1, + "pri_val": { + "Ua": 6000.0, + "Ub": 6002.5, + "Uc": 5998.3, + "Ia": 150.2, + "Ib": 148.9, + "Ic": 151.6, + "Pa": 820.5, + "Pb": 815.3, + "Pc": 825.7, + "Pt": 2461.5, + "Qa": 120.3, + "Qb": 118.6, + "Qc": 122.9, + "Qt": 361.8, + "Sa": 835.2, + "Sb": 829.7, + "Sc": 840.3, + "St": 2505.2, + "PFa": 0.98, + "PFb": 0.97, + "PFc": 0.99, + "PFt": 0.98, + "Uab": 10400.1, + "Ubc": 10395.6, + "Uca": 10398.2, + "frq": 50.00 + }, + "sec_val": { + "Ua": 57.6, + "Ub": 57.7, + "Uc": 57.5, + "Ia": 1.2, + "Ib": 1.18, + "Ic": 1.22, + "Pa": 68.2, + "Pb": 67.8, + "Pc": 68.6, + "Pt": 204.6, + "Qa": 9.8, + "Qb": 9.6, + "Qc": 10.1, + "Qt": 29.5, + "Sa": 69.5, + "Sb": 68.9, + "Sc": 70.1, + "St": 208.5, + "PFa": 0.98, + "PFb": 0.97, + "PFc": 0.99, + "PFt": 0.98, + "Uab": 100.0, + "Ubc": 99.8, + "Uca": 99.9, + "frq": 50.00 + } + }, + { + "line_no": 2, + "pri_val": {}, + "sec_val": {} + }, + { + "line_no": 3, + "pri_val": {}, + "sec_val": {} + }, + { + "line_no": 4, + "pri_val": {}, + "sec_val": {} + } + ], + "switch": { + "di1": 1, + "di2": 0, + "di3": 1, + "di4": 0, + "di5": 1, + "di6": 0, + "di7": 1, + "di8": 0, + "di9": 1, + "di10": 0, + "di11": 1, + "di12": 0, + "do1": 1, + "do2": 0, + "do3": 1, + "do4": 0, + "do5": 1, + "do6": 0, + "do7": 1, + "do8": 0, + "do9": 1, + "do10": 0, + "do11": 1, + "do12": 0 + }, + "ai_collect": { + "ai1": 4.52, + "ai2": 3.86, + "ai3": 5.21, + "ai4": 2.99, + "ai5": 6.10, + "ai6": 1.75, + "ai7": 3.22, + "ai8": 4.91, + "ai9": 2.18, + "ai10": 5.47, + "ai11": 3.06, + "ai12": 4.33 + } + } +} +``` + +--- + +#### 2. GET /api/device-status +**作用**:获取设备当前运行状态 +**参数**:无 +**返回data**: +```json +{ + "self_check": "正常", + "net1": "正常", + "net2": "正常", + "uart1": "正常", + "uart2": "断开" +} +``` + +--- + +#### 3. POST /api/config/device +**作用**:提交设备基础配置(版本、通讯、密码) +**请求body参数**: +```json +{ + "password": "123456", + "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": "", + "mask": "", + "gateway": "", + "protocol": "" + }, + { + "nic": "网卡二", + "ip": "192.168.1.56", + "mask": "255.255.255.255", + "gateway": "192.168.1.56", + "protocol": "Modbus TCP" + } + ], + "uart": [ + { + "port": "COM1", + "baud": 0, + "parity": "", + "data_bits": 0, + "stop_bits": 0, + "protocol": "" + }, + { + "port": "COM2", + "baud": 115200, + "parity": "NONE", + "data_bits": 8, + "stop_bits": 1, + "protocol": "Modbus RTU" + } + ] +} +``` +**返回data**:`{"save_path":"/config/device.json","send_status":"成功"}` + +--- + +#### 4. POST /api/config/channel +**作用**:提交AI/AO通道配置(AI:12通道,AO:12通道) +**请求body参数**: + +```json +{ + "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**:`{"save_path":"/config/channel.json","send_status":"成功"}` + +--- + +#### 5. POST /api/config/line_alarm_setting +**作用**:提交定值报警阈值配置 +**请求body参数**: + +```json +{ + "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": true + }, + { + "category": "频率", + "limit": 180, + "delay": 180, + "output_node": "开出1", + "enabled": false + } + ], + "fault_alarm": [ + { + "category": "PT断线", + "delay": 180, + "output_node": "开出1", + "enabled": true + }, + { + "category": "CT断线", + "delay": 180, + "output_node": "开出1", + "enabled": false + } + ] + } +``` +**返回data**:`{"save_path":"/config/setting.json","send_status":"成功"}` + +--- + +#### 5. POST /api/config/ai_alarm_setting + +**作用**:提交AI报警设置 +**请求body参数**: + +```json +[ + { + "channel_no": 1, + "singal_type": "4-20mA", + "limit_low":0, + "limit_high":20, + "delay": 180, + "output_node": "开出1", + "enabled": true + }, + { + "channel_no": 2, + "singal_type": "1-5v", + "limit_low":0, + "limit_high":20, + "delay": 180, + "output_node": "开出1", + "enabled": true + }, + ] +``` + +**返回data**:`{"save_path":"/config/setting.json","send_status":"成功"}` + +#### 6. POST /api/config/system + +**作用**:提交系统对时、灯光配置 +**请求body参数**: +```json +{ + "time_sync": "auto", + "brightness": 80, + "screen_saver": 60 +} +``` +**返回data**:`{"send_status":"成功"}` + +--- + +#### 7. GET /api/alarm/list +**作用**:分页查询历史报警 +**参数**:`page=1&size=20` +**返回data**: +```json +[ +{ + "id": 1, + "alarm_type": "operate_alarm", + "time": "2025-01-01 12:00:00", + "no": "", + "type": "", + "content": "告警内容", + "level": "告警等级" +} +] +``` + +--- + +#### 8. POST /api/control/switch +**作用**:下发开关量控制指令 +**请求body参数**: +```json +{"ch":1,"action":1} // 1=合,0=分 +``` +**返回data**:`{"control_status":"执行成功"}` + +--- + +### 4.2 WebSocket 接口(主动推送) +#### 1. WebSocket 地址:`ws://ip:port/ws/real-time` +**推送内容**:实时数据(0.5秒/次) +```json +{ + "type": "real_time", + "data": { + "line_list": [ + { + "line_no": 1, + "pri_val": { + "Ua": 6000.0, + "Ub": 6002.5, + "Uc": 5998.3, + "Ia": 150.2, + "Ib": 148.9, + "Ic": 151.6, + "Pa": 820.5, + "Pb": 815.3, + "Pc": 825.7, + "Pt": 2461.5, + "Qa": 120.3, + "Qb": 118.6, + "Qc": 122.9, + "Qt": 361.8, + "Sa": 835.2, + "Sb": 829.7, + "Sc": 840.3, + "St": 2505.2, + "PFa": 0.98, + "PFb": 0.97, + "PFc": 0.99, + "PFt": 0.98, + "Uab": 10400.1, + "Ubc": 10395.6, + "Uca": 10398.2, + "frq": 50.00 + }, + "sec_val": { + "Ua": 57.6, + "Ub": 57.7, + "Uc": 57.5, + "Ia": 1.2, + "Ib": 1.18, + "Ic": 1.22, + "Pa": 68.2, + "Pb": 67.8, + "Pc": 68.6, + "Pt": 204.6, + "Qa": 9.8, + "Qb": 9.6, + "Qc": 10.1, + "Qt": 29.5, + "Sa": 69.5, + "Sb": 68.9, + "Sc": 70.1, + "St": 208.5, + "PFa": 0.98, + "PFb": 0.97, + "PFc": 0.99, + "PFt": 0.98, + "Uab": 100.0, + "Ubc": 99.8, + "Uca": 99.9, + "frq": 50.00 + } + }, + { + "line_no": 2, + "pri_val": {}, + "sec_val": {} + }, + { + "line_no": 3, + "pri_val": {}, + "sec_val": {} + }, + { + "line_no": 4, + "pri_val": {}, + "sec_val": {} + } + ], + "switch": { + "di1": 1, + "di2": 0, + "di3": 1, + "di4": 0, + "di5": 1, + "di6": 0, + "di7": 1, + "di8": 0, + "di9": 1, + "di10": 0, + "di11": 1, + "di12": 0, + "do1": 1, + "do2": 0, + "do3": 1, + "do4": 0, + "do5": 1, + "do6": 0, + "do7": 1, + "do8": 0, + "do9": 1, + "do10": 0, + "do11": 1, + "do12": 0 + }, + "ai_collect": { + "ai1": 4.52, + "ai2": 3.86, + "ai3": 5.21, + "ai4": 2.99, + "ai5": 6.10, + "ai6": 1.75, + "ai7": 3.22, + "ai8": 4.91, + "ai9": 2.18, + "ai10": 5.47, + "ai11": 3.06, + "ai12": 4.33 + } + } +} +``` + +#### 2. WebSocket 地址:`ws://ip:port/ws/alarm` +**推送内容**:实时报警事件(触发即推) +```json +{ + "alarm_type": "line_alarm", + "time": "2025-01-01 12:00:00", + "no": "1", + "type": "", + "content": "告警内容", + "level": "告警等级" +}, +{ + "alarm_type": "ai_alarm", + "time": "2025-01-01 12:00:00", + "no": "2", + "type": "", + "content": "告警内容", + "level": "告警等级" +}, +{ + "alarm_type": "operate_alarm", + "time": "2025-01-01 12:00:00", + "no": "3", + "type": "", + "content": "告警内容", + "level": "告警等级" +} +``` + +## 五、数据存储设计 +### 5.1 JSON配置文件 +- 路径:`/config/` +- 文件:`device.json`、`channel.json`、`setting.json` + +### 5.2 SQLite报警数据库 +表:`alarm_event` +| 字段 | 类型 | 说明 | +|------|------|------| +| id | INTEGER | 主键 | +| alarm_type | TEXT | 告警类型(line_alarm,ai_alarm,operate_alarm) | +| time | DATETIME | 事件发生时间 | +| no | TEXT | 线路号/通道号 | +| type | TEXT | 类型 | +| content | TEXT | 告警详情 | +| level | TEXT | 告警等级 | + +## 六、安全与可靠性设计 +1. **密码加密存储**,接口权限校验 +2. WebSocket断线自动重连 +3. 配置文件修改自动备份 +4. 服务异常自动重启 + +## 七、总结 +本方案基于**RK3568平台**,采用**FastAPI + HTTP + WebSocket**架构,满足电气量测控平台全部需求: +- RESTful API 负责配置、查询、控制 +- WebSocket 负责实时数据、报警数据主动推送 +- JSON 存储配置,SQLite 存储报警 +- 0.5秒实时刷新,界面无卡顿、数据不丢失 + +系统架构轻量化、高性能,完全适配嵌入式平台运行。 + +--- + diff --git a/document/电气量测控平台系统需求分析及技术框架设计.pdf b/document/电气量测控平台系统需求分析及技术框架设计.pdf new file mode 100644 index 0000000..261a02c Binary files /dev/null and b/document/电气量测控平台系统需求分析及技术框架设计.pdf differ diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..9984d21 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + +
+ + +RK3568 + FastAPI + WebSocket + Vue 的前后端开发框架骨架。
+| 类型 | 时间 | 编号 | 内容 | 级别 |
|---|---|---|---|---|
| {{ alarm.alarm_type }} | +{{ alarm.time }} | +{{ alarm.no }} | +{{ alarm.content }} | +{{ alarm.level }} | +
当前页:{{ store.alarmPage }},最后刷新:{{ store.alarmUpdatedAt || '--' }}
+保存结果:{{ result }}
+| 通道 | 信号类型 | 线路 | 测量类型 | 下限 | 上限 |
|---|---|---|---|---|---|
| AI{{ form.ai_channel[0].ch }} | ++ | + | + | + | + |
| AO{{ form.ao_channel[0].ch }} | ++ | + | + | + | + |
保存结果:{{ result }}
+执行结果:{{ result }}
+保存结果:{{ result }}
+正在等待实时数据...
+最后刷新:{{ store.statusUpdatedAt || '--' }}
+ +正在加载状态...
+保存结果:{{ result }}
+