Initial commit
This commit is contained in:
commit
b8974dce59
50
.gitignore
vendored
Normal file
50
.gitignore
vendored
Normal file
@ -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
|
||||
|
||||
454
.trae/skills/python-fullstack-scaffold/SKILL.md
Normal file
454
.trae/skills/python-fullstack-scaffold/SKILL.md
Normal file
@ -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/`
|
||||
- 前端当前使用的具体技术栈和构建工具
|
||||
- 后端是否已有可复用入口、配置系统和测试结构
|
||||
- 用户要的是“技能文件”、 “设计方案” 还是 “直接生成代码”
|
||||
|
||||
若信息不足,先补充上下文;若仓库已有实现,优先做兼容式演进,而不是重建。
|
||||
16
backend/README.md
Normal file
16
backend/README.md
Normal file
@ -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`
|
||||
1
backend/app/__init__.py
Normal file
1
backend/app/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
||||
157
backend/app/adapters/device_client.py
Normal file
157
backend/app/adapters/device_client.py
Normal file
@ -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 行为,后续在此类中替换协议实现。"""
|
||||
1
backend/app/api/__init__.py
Normal file
1
backend/app/api/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
||||
12
backend/app/api/router.py
Normal file
12
backend/app/api/router.py
Normal file
@ -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)
|
||||
1
backend/app/api/routes/__init__.py
Normal file
1
backend/app/api/routes/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
||||
13
backend/app/api/routes/alarms.py
Normal file
13
backend/app/api/routes/alarms.py
Normal file
@ -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="获取报警历史成功")
|
||||
16
backend/app/api/routes/auth.py
Normal file
16
backend/app/api/routes/auth.py
Normal file
@ -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="密码校验完成")
|
||||
36
backend/app/api/routes/config.py
Normal file
36
backend/app/api/routes/config.py
Normal file
@ -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))
|
||||
14
backend/app/api/routes/control.py
Normal file
14
backend/app/api/routes/control.py
Normal file
@ -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))
|
||||
13
backend/app/api/routes/realtime.py
Normal file
13
backend/app/api/routes/realtime.py
Normal file
@ -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="获取实时数据成功")
|
||||
13
backend/app/api/routes/status.py
Normal file
13
backend/app/api/routes/status.py
Normal file
@ -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="获取设备状态成功")
|
||||
37
backend/app/cache/memory_store.py
vendored
Normal file
37
backend/app/cache/memory_store.py
vendored
Normal file
@ -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()
|
||||
31
backend/app/core/config.py
Normal file
31
backend/app/core/config.py
Normal file
@ -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()
|
||||
9
backend/app/core/response.py
Normal file
9
backend/app/core/response.py
Normal file
@ -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 {},
|
||||
}
|
||||
27
backend/app/core/security.py
Normal file
27
backend/app/core/security.py
Normal file
@ -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="无效的访问令牌",
|
||||
)
|
||||
33
backend/app/db/sqlite.py
Normal file
33
backend/app/db/sqlite.py
Normal file
@ -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()
|
||||
65
backend/app/main.py
Normal file
65
backend/app/main.py
Normal file
@ -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)
|
||||
41
backend/app/repositories/alarm_repo.py
Normal file
41
backend/app/repositories/alarm_repo.py
Normal file
@ -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()]
|
||||
39
backend/app/repositories/json_config_repo.py
Normal file
39
backend/app/repositories/json_config_repo.py
Normal file
@ -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)
|
||||
14
backend/app/schemas/common.py
Normal file
14
backend/app/schemas/common.py
Normal file
@ -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)
|
||||
158
backend/app/schemas/platform.py
Normal file
158
backend/app/schemas/platform.py
Normal file
@ -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)
|
||||
105
backend/app/services/platform_service.py
Normal file
105
backend/app/services/platform_service.py
Normal file
@ -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()
|
||||
22
backend/app/tasks/polling.py
Normal file
22
backend/app/tasks/polling.py
Normal file
@ -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
|
||||
14
backend/app/utils/backup.py
Normal file
14
backend/app/utils/backup.py
Normal file
@ -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)
|
||||
32
backend/app/ws/manager.py
Normal file
32
backend/app/ws/manager.py
Normal file
@ -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()
|
||||
22
backend/config/channel.json
Normal file
22
backend/config/channel.json
Normal file
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
47
backend/config/device.json
Normal file
47
backend/config/device.json
Normal file
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
39
backend/config/setting.json
Normal file
39
backend/config/setting.json
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
26
backend/pyproject.toml
Normal file
26
backend/pyproject.toml
Normal file
@ -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"]
|
||||
21
backend/setup.py
Normal file
21
backend/setup.py
Normal file
@ -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",
|
||||
]
|
||||
},
|
||||
)
|
||||
20
backend/tests/test_api.py
Normal file
20
backend/tests/test_api.py
Normal file
@ -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"]
|
||||
207
document/gui.h
Normal file
207
document/gui.h
Normal file
@ -0,0 +1,207 @@
|
||||
#ifndef __GUI_H
|
||||
#define __GUI_H
|
||||
|
||||
#include "time.h"
|
||||
|
||||
#define LINE_TOTAL_NUM 4 //线路数量
|
||||
#define ANALOG_CH_NUM 12 //AI、AO模拟量通道数
|
||||
|
||||
|
||||
/*************上行数据*************************************************小核数据 -> 大核****************************************************************************/
|
||||
|
||||
//实时量
|
||||
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个通道:ua、ub、uc, ia、ib、ic、频率系数、开入量
|
||||
//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
|
||||
422
document/前后端启动说明.md
Normal file
422
document/前后端启动说明.md
Normal file
@ -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 反向代理说明
|
||||
579
document/在RK3568设备上系统部署及使用说明.md
Normal file
579
document/在RK3568设备上系统部署及使用说明.md
Normal file
@ -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 证书
|
||||
- 真实硬件采集驱动接入
|
||||
- 自动备份配置与报警数据库
|
||||
BIN
document/在RK3568设备上系统部署及使用说明.pdf
Normal file
BIN
document/在RK3568设备上系统部署及使用说明.pdf
Normal file
Binary file not shown.
BIN
document/电气量测控平台-显示界面设计和数据接口说.docx
Normal file
BIN
document/电气量测控平台-显示界面设计和数据接口说.docx
Normal file
Binary file not shown.
BIN
document/电气量测控平台-显示界面设计和数据接口说.pdf
Normal file
BIN
document/电气量测控平台-显示界面设计和数据接口说.pdf
Normal file
Binary file not shown.
689
document/电气量测控平台系统需求分析及技术框架设计.md
Normal file
689
document/电气量测控平台系统需求分析及技术框架设计.md
Normal file
@ -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秒实时刷新,界面无卡顿、数据不丢失
|
||||
|
||||
系统架构轻量化、高性能,完全适配嵌入式平台运行。
|
||||
|
||||
---
|
||||
|
||||
BIN
document/电气量测控平台系统需求分析及技术框架设计.pdf
Normal file
BIN
document/电气量测控平台系统需求分析及技术框架设计.pdf
Normal file
Binary file not shown.
12
frontend/index.html
Normal file
12
frontend/index.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>电气量测控平台</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
1832
frontend/package-lock.json
generated
Normal file
1832
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
frontend/package.json
Normal file
21
frontend/package.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "emcp-frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.7.2",
|
||||
"vue": "^3.5.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^6.0.5",
|
||||
"vue-tsc": "^2.2.0"
|
||||
}
|
||||
}
|
||||
54
frontend/src/App.vue
Normal file
54
frontend/src/App.vue
Normal file
@ -0,0 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { usePlatformStore } from './stores/platform'
|
||||
import RealtimeView from './views/RealtimeView.vue'
|
||||
import StatusView from './views/StatusView.vue'
|
||||
import DeviceConfigView from './views/DeviceConfigView.vue'
|
||||
import ChannelConfigView from './views/ChannelConfigView.vue'
|
||||
import AlarmSettingView from './views/AlarmSettingView.vue'
|
||||
import AlarmHistoryView from './views/AlarmHistoryView.vue'
|
||||
import ControlView from './views/ControlView.vue'
|
||||
import SystemConfigView from './views/SystemConfigView.vue'
|
||||
|
||||
const store = usePlatformStore()
|
||||
const activeTab = ref('realtime')
|
||||
|
||||
const tabs = [
|
||||
{ key: 'realtime', label: '实时数据', component: RealtimeView },
|
||||
{ key: 'status', label: '设备状态', component: StatusView },
|
||||
{ key: 'device', label: '设备配置', component: DeviceConfigView },
|
||||
{ key: 'channel', label: '通道配置', component: ChannelConfigView },
|
||||
{ key: 'alarm-setting', label: '报警设置', component: AlarmSettingView },
|
||||
{ key: 'alarms', label: '报警历史', component: AlarmHistoryView },
|
||||
{ key: 'control', label: '控制指令', component: ControlView },
|
||||
{ key: 'system', label: '系统设置', component: SystemConfigView },
|
||||
]
|
||||
|
||||
const currentComponent = computed(() => tabs.find((item) => item.key === activeTab.value)?.component ?? RealtimeView)
|
||||
|
||||
onMounted(() => {
|
||||
void store.bootstrap()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="app-shell">
|
||||
<header class="page-header">
|
||||
<h1>电气量测控平台</h1>
|
||||
<p>RK3568 + FastAPI + WebSocket + Vue 的前后端开发框架骨架。</p>
|
||||
</header>
|
||||
|
||||
<section class="tabs">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
:class="{ active: tab.key === activeTab }"
|
||||
@click="activeTab = tab.key"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<component :is="currentComponent" :store="store.state" :actions="store" />
|
||||
</main>
|
||||
</template>
|
||||
10
frontend/src/api/http.ts
Normal file
10
frontend/src/api/http.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import axios from 'axios'
|
||||
|
||||
export const http = axios.create({
|
||||
baseURL: 'http://127.0.0.1:8000/api',
|
||||
timeout: 5000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-API-Token': 'admin123',
|
||||
},
|
||||
})
|
||||
71
frontend/src/api/platform.ts
Normal file
71
frontend/src/api/platform.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { http } from './http'
|
||||
import type {
|
||||
AiAlarmSettingItem,
|
||||
AlarmEvent,
|
||||
ApiResponse,
|
||||
ChannelConfigPayload,
|
||||
DeviceConfigPayload,
|
||||
DeviceStatus,
|
||||
LineAlarmSettingPayload,
|
||||
RealtimeData,
|
||||
SystemConfigPayload,
|
||||
} from '../types/platform'
|
||||
|
||||
export async function fetchRealtimeData(): Promise<RealtimeData> {
|
||||
const response = await http.get<ApiResponse<RealtimeData>>('/real-time-data')
|
||||
return response.data.data
|
||||
}
|
||||
|
||||
export async function fetchDeviceStatus(): Promise<DeviceStatus> {
|
||||
const response = await http.get<ApiResponse<DeviceStatus>>('/device-status')
|
||||
return response.data.data
|
||||
}
|
||||
|
||||
export async function fetchAlarmHistory(page = 1, size = 20): Promise<AlarmEvent[]> {
|
||||
const response = await http.get<ApiResponse<AlarmEvent[]>>(`/alarm/list?page=${page}&size=${size}`)
|
||||
return response.data.data
|
||||
}
|
||||
|
||||
export async function verifyAccessPassword(password: string): Promise<boolean> {
|
||||
const response = await http.post<ApiResponse<boolean>>('/auth/verify-password', { password })
|
||||
return response.data.data
|
||||
}
|
||||
|
||||
export async function saveDeviceConfig(payload: DeviceConfigPayload): Promise<ApiResponse<{ save_path: string; send_status: string }>> {
|
||||
const response = await http.post<ApiResponse<{ save_path: string; send_status: string }>>('/config/device', payload)
|
||||
return response.data
|
||||
}
|
||||
|
||||
export async function saveChannelConfig(
|
||||
payload: ChannelConfigPayload,
|
||||
): Promise<ApiResponse<{ save_path: string; send_status: string }>> {
|
||||
const response = await http.post<ApiResponse<{ save_path: string; send_status: string }>>('/config/channel', payload)
|
||||
return response.data
|
||||
}
|
||||
|
||||
export async function saveLineAlarmSetting(
|
||||
payload: LineAlarmSettingPayload,
|
||||
): Promise<ApiResponse<{ save_path: string; send_status: string }>> {
|
||||
const response = await http.post<ApiResponse<{ save_path: string; send_status: string }>>('/config/line_alarm_setting', payload)
|
||||
return response.data
|
||||
}
|
||||
|
||||
export async function saveAiAlarmSetting(
|
||||
payload: AiAlarmSettingItem[],
|
||||
): Promise<ApiResponse<{ save_path: string; send_status: string; target: string; items: number }>> {
|
||||
const response = await http.post<ApiResponse<{ save_path: string; send_status: string; target: string; items: number }>>(
|
||||
'/config/ai_alarm_setting',
|
||||
payload,
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
|
||||
export async function controlSwitch(ch: number, action: number): Promise<ApiResponse<{ control_status: string }>> {
|
||||
const response = await http.post<ApiResponse<{ control_status: string }>>('/control/switch', { ch, action })
|
||||
return response.data
|
||||
}
|
||||
|
||||
export async function saveSystemConfig(payload: SystemConfigPayload): Promise<ApiResponse<{ send_status: string }>> {
|
||||
const response = await http.post<ApiResponse<{ send_status: string }>>('/config/system', payload)
|
||||
return response.data
|
||||
}
|
||||
5
frontend/src/main.ts
Normal file
5
frontend/src/main.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import './style.css'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
142
frontend/src/stores/platform.ts
Normal file
142
frontend/src/stores/platform.ts
Normal file
@ -0,0 +1,142 @@
|
||||
import { reactive } from 'vue'
|
||||
import { fetchAlarmHistory, fetchDeviceStatus, fetchRealtimeData } from '../api/platform'
|
||||
import { connectAlarm, connectRealtimeWithLifecycle } from '../websocket/client'
|
||||
import type { AlarmEvent, DeviceStatus, RealtimeData } from '../types/platform'
|
||||
|
||||
interface PlatformState {
|
||||
realtime: RealtimeData | null
|
||||
status: DeviceStatus | null
|
||||
alarms: AlarmEvent[]
|
||||
alarmPage: number
|
||||
alarmPageSize: number
|
||||
alarmHasNext: boolean
|
||||
alarmLoading: boolean
|
||||
realtimeConnected: boolean
|
||||
realtimeSource: string
|
||||
realtimeUpdatedAt: string
|
||||
statusLoading: boolean
|
||||
statusUpdatedAt: string
|
||||
alarmUpdatedAt: string
|
||||
}
|
||||
|
||||
const state = reactive<PlatformState>({
|
||||
realtime: null,
|
||||
status: null,
|
||||
alarms: [],
|
||||
alarmPage: 1,
|
||||
alarmPageSize: 20,
|
||||
alarmHasNext: false,
|
||||
alarmLoading: false,
|
||||
realtimeConnected: false,
|
||||
realtimeSource: 'init',
|
||||
realtimeUpdatedAt: '',
|
||||
statusLoading: false,
|
||||
statusUpdatedAt: '',
|
||||
alarmUpdatedAt: '',
|
||||
})
|
||||
|
||||
let realtimeSocket: WebSocket | null = null
|
||||
let alarmSocket: WebSocket | null = null
|
||||
let realtimePollTimer: number | null = null
|
||||
|
||||
function markRealtime(data: RealtimeData, source: string) {
|
||||
state.realtime = data
|
||||
state.realtimeSource = source
|
||||
state.realtimeUpdatedAt = new Date().toLocaleTimeString()
|
||||
}
|
||||
|
||||
async function syncRealtimeByHttp(source = 'http-poll') {
|
||||
const realtime = await fetchRealtimeData()
|
||||
markRealtime(realtime, source)
|
||||
}
|
||||
|
||||
async function refreshStatus() {
|
||||
state.statusLoading = true
|
||||
try {
|
||||
state.status = await fetchDeviceStatus()
|
||||
state.statusUpdatedAt = new Date().toLocaleTimeString()
|
||||
} finally {
|
||||
state.statusLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAlarms(page = state.alarmPage) {
|
||||
state.alarmLoading = true
|
||||
try {
|
||||
const alarms = await fetchAlarmHistory(page, state.alarmPageSize)
|
||||
state.alarms = alarms
|
||||
state.alarmPage = page
|
||||
state.alarmHasNext = alarms.length === state.alarmPageSize
|
||||
state.alarmUpdatedAt = new Date().toLocaleTimeString()
|
||||
} finally {
|
||||
state.alarmLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
async function nextAlarmPage() {
|
||||
if (!state.alarmHasNext) {
|
||||
return
|
||||
}
|
||||
await refreshAlarms(state.alarmPage + 1)
|
||||
}
|
||||
|
||||
async function prevAlarmPage() {
|
||||
if (state.alarmPage <= 1) {
|
||||
return
|
||||
}
|
||||
await refreshAlarms(state.alarmPage - 1)
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
await syncRealtimeByHttp('http-init')
|
||||
await refreshStatus()
|
||||
await refreshAlarms(1)
|
||||
|
||||
if (!realtimeSocket) {
|
||||
realtimeSocket = connectRealtimeWithLifecycle(
|
||||
(data) => {
|
||||
state.realtimeConnected = true
|
||||
markRealtime(data, 'websocket')
|
||||
},
|
||||
{
|
||||
onOpen: () => {
|
||||
state.realtimeConnected = true
|
||||
},
|
||||
onClose: () => {
|
||||
state.realtimeConnected = false
|
||||
},
|
||||
onError: () => {
|
||||
state.realtimeConnected = false
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (!alarmSocket) {
|
||||
alarmSocket = connectAlarm((alarm) => {
|
||||
if (state.alarmPage === 1) {
|
||||
state.alarms = [alarm, ...state.alarms].slice(0, state.alarmPageSize)
|
||||
state.alarmUpdatedAt = new Date().toLocaleTimeString()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (!realtimePollTimer) {
|
||||
realtimePollTimer = window.setInterval(() => {
|
||||
if (!state.realtimeConnected) {
|
||||
void syncRealtimeByHttp('http-fallback')
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
export function usePlatformStore() {
|
||||
return {
|
||||
state,
|
||||
bootstrap,
|
||||
refreshStatus,
|
||||
refreshAlarms,
|
||||
nextAlarmPage,
|
||||
prevAlarmPage,
|
||||
}
|
||||
}
|
||||
173
frontend/src/style.css
Normal file
173
frontend/src/style.css
Normal file
@ -0,0 +1,173 @@
|
||||
:root {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
color: #0f172a;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
select {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: #ffffff;
|
||||
border: 1px solid #dbe3ef;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.05);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.tabs button {
|
||||
border: 1px solid #cbd5e1;
|
||||
background: #ffffff;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tabs button.active {
|
||||
background: #2563eb;
|
||||
color: #ffffff;
|
||||
border-color: #2563eb;
|
||||
}
|
||||
|
||||
.metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.metric {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.metric .label {
|
||||
color: #475569;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.metric .value {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.status-ok {
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
.status-warn {
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.form-grid label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-grid input,
|
||||
.form-grid select {
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.table th,
|
||||
.table td {
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
padding: 8px 4px;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.table input,
|
||||
.table select {
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.primary {
|
||||
border: none;
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.primary:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: #475569;
|
||||
font-size: 13px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
127
frontend/src/types/platform.ts
Normal file
127
frontend/src/types/platform.ts
Normal file
@ -0,0 +1,127 @@
|
||||
export interface ValueGroup {
|
||||
Ua: number
|
||||
Ub: number
|
||||
Uc: number
|
||||
Ia: number
|
||||
Ib: number
|
||||
Ic: number
|
||||
Pt: number
|
||||
frq: number
|
||||
}
|
||||
|
||||
export interface LineData {
|
||||
line_no: number
|
||||
pri_val: ValueGroup
|
||||
sec_val: ValueGroup
|
||||
}
|
||||
|
||||
export interface RealtimeData {
|
||||
line_list: LineData[]
|
||||
switch: Record<string, number>
|
||||
ai_collect: Record<string, number>
|
||||
}
|
||||
|
||||
export interface DeviceStatus {
|
||||
self_check: string
|
||||
net1: string
|
||||
net2: string
|
||||
uart1: string
|
||||
uart2: string
|
||||
}
|
||||
|
||||
export interface AlarmEvent {
|
||||
id?: number
|
||||
alarm_type: string
|
||||
time: string
|
||||
no: string
|
||||
type: string
|
||||
content: string
|
||||
level: string
|
||||
}
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
code: number
|
||||
msg: string
|
||||
data: T
|
||||
}
|
||||
|
||||
export interface HardwareVersion {
|
||||
board_version: string
|
||||
display_version: string
|
||||
other_version: string
|
||||
}
|
||||
|
||||
export interface SoftwareVersion {
|
||||
display_program: string
|
||||
communication_program: string
|
||||
measurement_program: string
|
||||
}
|
||||
|
||||
export interface NetConfigItem {
|
||||
nic: string
|
||||
ip: string
|
||||
mask: string
|
||||
gateway: string
|
||||
protocol: string
|
||||
}
|
||||
|
||||
export interface UartConfigItem {
|
||||
port: string
|
||||
baud: number
|
||||
parity: string
|
||||
data_bits: number
|
||||
stop_bits: number
|
||||
protocol: string
|
||||
}
|
||||
|
||||
export interface DeviceConfigPayload {
|
||||
password: string
|
||||
hardware_version: HardwareVersion
|
||||
software_version: SoftwareVersion
|
||||
net: NetConfigItem[]
|
||||
uart: UartConfigItem[]
|
||||
}
|
||||
|
||||
export interface ChannelItem {
|
||||
ch: number
|
||||
singal_type: string
|
||||
line_no: number
|
||||
type: string
|
||||
limit_low: number
|
||||
limit_high: number
|
||||
}
|
||||
|
||||
export interface ChannelConfigPayload {
|
||||
ai_channel: ChannelItem[]
|
||||
ao_channel: ChannelItem[]
|
||||
}
|
||||
|
||||
export interface AlarmRule {
|
||||
category: string
|
||||
limit?: number
|
||||
delay: number
|
||||
output_node: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface LineAlarmSettingPayload {
|
||||
line_no: number
|
||||
over_limit_alarm: AlarmRule[]
|
||||
fault_alarm: AlarmRule[]
|
||||
}
|
||||
|
||||
export interface AiAlarmSettingItem {
|
||||
channel_no: number
|
||||
singal_type: string
|
||||
limit_low: number
|
||||
limit_high: number
|
||||
delay: number
|
||||
output_node: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface SystemConfigPayload {
|
||||
time_sync: string
|
||||
brightness: number
|
||||
screen_saver: number
|
||||
}
|
||||
20
frontend/src/utils/saveGuard.ts
Normal file
20
frontend/src/utils/saveGuard.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { verifyAccessPassword } from '../api/platform'
|
||||
|
||||
export async function ensureSaveAuthorized(): Promise<{ ok: boolean; message: string }> {
|
||||
const password = window.prompt('请输入安全校验密码')
|
||||
|
||||
if (password === null) {
|
||||
return { ok: false, message: '已取消安全校验' }
|
||||
}
|
||||
|
||||
if (!password.trim()) {
|
||||
return { ok: false, message: '安全校验密码不能为空' }
|
||||
}
|
||||
|
||||
const verified = await verifyAccessPassword(password)
|
||||
if (!verified) {
|
||||
return { ok: false, message: '安全校验失败,密码错误' }
|
||||
}
|
||||
|
||||
return { ok: true, message: '安全校验通过' }
|
||||
}
|
||||
33
frontend/src/views/AlarmHistoryView.vue
Normal file
33
frontend/src/views/AlarmHistoryView.vue
Normal file
@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{ store: any; actions: any }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="panel">
|
||||
<div class="section-header">
|
||||
<h2>报警历史</h2>
|
||||
<div class="actions">
|
||||
<button class="primary" :disabled="store.alarmLoading" @click="actions.refreshAlarms()">
|
||||
{{ store.alarmLoading ? '刷新中...' : '刷新列表' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead><tr><th>类型</th><th>时间</th><th>编号</th><th>内容</th><th>级别</th></tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="alarm in store.alarms" :key="`${alarm.time}-${alarm.no}-${alarm.content}`">
|
||||
<td>{{ alarm.alarm_type }}</td>
|
||||
<td>{{ alarm.time }}</td>
|
||||
<td>{{ alarm.no }}</td>
|
||||
<td>{{ alarm.content }}</td>
|
||||
<td>{{ alarm.level }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="actions">
|
||||
<button class="primary" :disabled="store.alarmLoading || store.alarmPage <= 1" @click="actions.prevAlarmPage()">上一页</button>
|
||||
<button class="primary" :disabled="store.alarmLoading || !store.alarmHasNext" @click="actions.nextAlarmPage()">下一页</button>
|
||||
</div>
|
||||
<p class="hint">当前页:{{ store.alarmPage }},最后刷新:{{ store.alarmUpdatedAt || '--' }}</p>
|
||||
</section>
|
||||
</template>
|
||||
76
frontend/src/views/AlarmSettingView.vue
Normal file
76
frontend/src/views/AlarmSettingView.vue
Normal file
@ -0,0 +1,76 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { saveAiAlarmSetting, saveLineAlarmSetting } from '../api/platform'
|
||||
import type { AiAlarmSettingItem, LineAlarmSettingPayload } from '../types/platform'
|
||||
import { ensureSaveAuthorized } from '../utils/saveGuard'
|
||||
|
||||
defineProps<{ store: any; actions: any }>()
|
||||
|
||||
const lineAlarm = reactive<LineAlarmSettingPayload>({
|
||||
line_no: 1,
|
||||
over_limit_alarm: [
|
||||
{ category: '电压', limit: 180, delay: 180, output_node: '开出1', enabled: true },
|
||||
{ category: '电流', limit: 180, delay: 180, output_node: '开出1', enabled: true },
|
||||
{ category: '差流', limit: 180, delay: 180, output_node: '开出1', enabled: false },
|
||||
{ category: '频率', limit: 180, delay: 180, output_node: '开出1', enabled: false },
|
||||
],
|
||||
fault_alarm: [{ category: 'PT断线', delay: 180, output_node: '开出1', enabled: true }],
|
||||
})
|
||||
|
||||
const aiAlarm = reactive<AiAlarmSettingItem[]>([
|
||||
{
|
||||
channel_no: 1,
|
||||
singal_type: '4-20mA',
|
||||
limit_low: 0,
|
||||
limit_high: 20,
|
||||
delay: 180,
|
||||
output_node: '开出1',
|
||||
enabled: true,
|
||||
},
|
||||
])
|
||||
|
||||
const result = ref('未保存')
|
||||
const saving = ref(false)
|
||||
|
||||
async function save() {
|
||||
saving.value = true
|
||||
try {
|
||||
const guard = await ensureSaveAuthorized()
|
||||
if (!guard.ok) {
|
||||
result.value = guard.message
|
||||
return
|
||||
}
|
||||
|
||||
const [lineResponse, aiResponse] = await Promise.all([saveLineAlarmSetting(lineAlarm), saveAiAlarmSetting(aiAlarm)])
|
||||
result.value = `${lineResponse.msg} / ${aiResponse.msg}`
|
||||
} catch (error) {
|
||||
result.value = error instanceof Error ? error.message : '保存报警设置失败'
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="panel">
|
||||
<h2>报警设置</h2>
|
||||
<div class="form-grid">
|
||||
<label>线路号<input v-model.number="lineAlarm.line_no" type="number" min="1" max="4" /></label>
|
||||
<label>电压越限<input v-model.number="lineAlarm.over_limit_alarm[0].limit" type="number" /></label>
|
||||
<label>电流越限<input v-model.number="lineAlarm.over_limit_alarm[1].limit" type="number" /></label>
|
||||
<label>差流越限<input v-model.number="lineAlarm.over_limit_alarm[2].limit" type="number" /></label>
|
||||
<label>频率越限<input v-model.number="lineAlarm.over_limit_alarm[3].limit" type="number" /></label>
|
||||
<label>动作延时<input v-model.number="lineAlarm.over_limit_alarm[0].delay" type="number" /></label>
|
||||
<label>输出节点<select v-model="lineAlarm.over_limit_alarm[0].output_node"><option>开出1</option><option>开出2</option></select></label>
|
||||
<label>PT断线延时<input v-model.number="lineAlarm.fault_alarm[0].delay" type="number" /></label>
|
||||
<label>AI通道号<input v-model.number="aiAlarm[0].channel_no" type="number" min="1" max="12" /></label>
|
||||
<label>AI信号类型<input v-model="aiAlarm[0].singal_type" /></label>
|
||||
<label>AI下限<input v-model.number="aiAlarm[0].limit_low" type="number" /></label>
|
||||
<label>AI上限<input v-model.number="aiAlarm[0].limit_high" type="number" /></label>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="primary" :disabled="saving" @click="save">{{ saving ? '保存中...' : '保存报警设置' }}</button>
|
||||
</div>
|
||||
<p>保存结果:{{ result }}</p>
|
||||
</section>
|
||||
</template>
|
||||
67
frontend/src/views/ChannelConfigView.vue
Normal file
67
frontend/src/views/ChannelConfigView.vue
Normal file
@ -0,0 +1,67 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { saveChannelConfig } from '../api/platform'
|
||||
import type { ChannelConfigPayload } from '../types/platform'
|
||||
import { ensureSaveAuthorized } from '../utils/saveGuard'
|
||||
|
||||
defineProps<{ store: any; actions: any }>()
|
||||
|
||||
const form = reactive<ChannelConfigPayload>({
|
||||
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 }],
|
||||
})
|
||||
|
||||
const result = ref('未保存')
|
||||
const saving = ref(false)
|
||||
|
||||
async function save() {
|
||||
saving.value = true
|
||||
try {
|
||||
const guard = await ensureSaveAuthorized()
|
||||
if (!guard.ok) {
|
||||
result.value = guard.message
|
||||
return
|
||||
}
|
||||
|
||||
const response = await saveChannelConfig(form)
|
||||
result.value = `${response.msg},${response.data.send_status}`
|
||||
} catch (error) {
|
||||
result.value = error instanceof Error ? error.message : '保存通道配置失败'
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="panel">
|
||||
<h2>通道配置</h2>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr><th>通道</th><th>信号类型</th><th>线路</th><th>测量类型</th><th>下限</th><th>上限</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>AI{{ form.ai_channel[0].ch }}</td>
|
||||
<td><input v-model="form.ai_channel[0].singal_type" /></td>
|
||||
<td><input v-model.number="form.ai_channel[0].line_no" type="number" min="1" max="4" /></td>
|
||||
<td><input v-model="form.ai_channel[0].type" /></td>
|
||||
<td><input v-model.number="form.ai_channel[0].limit_low" type="number" /></td>
|
||||
<td><input v-model.number="form.ai_channel[0].limit_high" type="number" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>AO{{ form.ao_channel[0].ch }}</td>
|
||||
<td><input v-model="form.ao_channel[0].singal_type" /></td>
|
||||
<td><input v-model.number="form.ao_channel[0].line_no" type="number" min="1" max="4" /></td>
|
||||
<td><input v-model="form.ao_channel[0].type" /></td>
|
||||
<td><input v-model.number="form.ao_channel[0].limit_low" type="number" /></td>
|
||||
<td><input v-model.number="form.ao_channel[0].limit_high" type="number" /></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="actions">
|
||||
<button class="primary" :disabled="saving" @click="save">{{ saving ? '保存中...' : '保存通道配置' }}</button>
|
||||
</div>
|
||||
<p>保存结果:{{ result }}</p>
|
||||
</section>
|
||||
</template>
|
||||
28
frontend/src/views/ControlView.vue
Normal file
28
frontend/src/views/ControlView.vue
Normal file
@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
import { controlSwitch } from "../api/platform"
|
||||
|
||||
defineProps<{ store: any; actions: any }>()
|
||||
|
||||
const channel = ref(1)
|
||||
const result = ref("等待下发")
|
||||
|
||||
async function sendControl(action: number) {
|
||||
const response = await controlSwitch(channel.value, action)
|
||||
result.value = response.data.control_status
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="panel">
|
||||
<h2>控制指令</h2>
|
||||
<div class="form-grid">
|
||||
<label>开关通道<input v-model="channel" type="number" min="1" max="12" /></label>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="primary" @click="sendControl(1)">合闸</button>
|
||||
<button class="primary" @click="sendControl(0)">分闸</button>
|
||||
</div>
|
||||
<p>执行结果:{{ result }}</p>
|
||||
</section>
|
||||
</template>
|
||||
78
frontend/src/views/DeviceConfigView.vue
Normal file
78
frontend/src/views/DeviceConfigView.vue
Normal file
@ -0,0 +1,78 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { saveDeviceConfig } from '../api/platform'
|
||||
import type { DeviceConfigPayload } from '../types/platform'
|
||||
import { ensureSaveAuthorized } from '../utils/saveGuard'
|
||||
|
||||
defineProps<{ store: any; actions: any }>()
|
||||
|
||||
const form = reactive<DeviceConfigPayload>({
|
||||
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: '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' },
|
||||
],
|
||||
})
|
||||
|
||||
const result = ref('未保存')
|
||||
const saving = ref(false)
|
||||
|
||||
async function save() {
|
||||
saving.value = true
|
||||
try {
|
||||
const guard = await ensureSaveAuthorized()
|
||||
if (!guard.ok) {
|
||||
result.value = guard.message
|
||||
return
|
||||
}
|
||||
|
||||
const response = await saveDeviceConfig(form)
|
||||
result.value = `${response.msg},${response.data.send_status}`
|
||||
} catch (error) {
|
||||
result.value = error instanceof Error ? error.message : '保存设备配置失败'
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="panel">
|
||||
<h2>设备基础配置</h2>
|
||||
<div class="form-grid">
|
||||
<label>操作密码<input v-model="form.password" type="password" /></label>
|
||||
<label>板卡版本<input v-model="form.hardware_version.board_version" /></label>
|
||||
<label>显示版本<input v-model="form.hardware_version.display_version" /></label>
|
||||
<label>其他版本<input v-model="form.hardware_version.other_version" /></label>
|
||||
<label>显示程序版本<input v-model="form.software_version.display_program" /></label>
|
||||
<label>通信程序版本<input v-model="form.software_version.communication_program" /></label>
|
||||
<label>测量程序版本<input v-model="form.software_version.measurement_program" /></label>
|
||||
<label>网卡一 IP<input v-model="form.net[0].ip" /></label>
|
||||
<label>网卡一 掩码<input v-model="form.net[0].mask" /></label>
|
||||
<label>网卡一 网关<input v-model="form.net[0].gateway" /></label>
|
||||
<label>网卡二 IP<input v-model="form.net[1].ip" /></label>
|
||||
<label>网卡二 协议<input v-model="form.net[1].protocol" /></label>
|
||||
<label>串口一波特率<input v-model.number="form.uart[0].baud" type="number" /></label>
|
||||
<label>串口二波特率<input v-model.number="form.uart[1].baud" type="number" /></label>
|
||||
<label>串口二协议<input v-model="form.uart[1].protocol" /></label>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="primary" :disabled="saving" @click="save">{{ saving ? '保存中...' : '保存设备配置' }}</button>
|
||||
</div>
|
||||
<p>保存结果:{{ result }}</p>
|
||||
</section>
|
||||
</template>
|
||||
35
frontend/src/views/RealtimeView.vue
Normal file
35
frontend/src/views/RealtimeView.vue
Normal file
@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{ store: any; actions: any }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="panel">
|
||||
<h2>实时数据</h2>
|
||||
<div class="metrics" style="margin-bottom: 12px">
|
||||
<div class="metric">
|
||||
<div class="label">数据源</div>
|
||||
<div class="value">{{ store.realtimeSource }}</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="label">连接状态</div>
|
||||
<div class="value">{{ store.realtimeConnected ? 'WebSocket 已连接' : 'HTTP 轮询回退' }}</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="label">最后更新时间</div>
|
||||
<div class="value">{{ store.realtimeUpdatedAt || '--' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="store.realtime" class="grid">
|
||||
<div v-for="line in store.realtime.line_list" :key="line.line_no" class="panel">
|
||||
<h3>线路 {{ line.line_no }}</h3>
|
||||
<div class="metrics">
|
||||
<div class="metric"><div class="label">一次电压 Ua</div><div class="value">{{ line.pri_val.Ua }}</div></div>
|
||||
<div class="metric"><div class="label">一次电流 Ia</div><div class="value">{{ line.pri_val.Ia }}</div></div>
|
||||
<div class="metric"><div class="label">总有功 Pt</div><div class="value">{{ line.pri_val.Pt }}</div></div>
|
||||
<div class="metric"><div class="label">频率</div><div class="value">{{ line.pri_val.frq }}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else>正在等待实时数据...</p>
|
||||
</section>
|
||||
</template>
|
||||
27
frontend/src/views/StatusView.vue
Normal file
27
frontend/src/views/StatusView.vue
Normal file
@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{ store: any; actions: any }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="panel">
|
||||
<div class="section-header">
|
||||
<h2>设备状态</h2>
|
||||
<div class="actions">
|
||||
<button class="primary" :disabled="store.statusLoading" @click="actions.refreshStatus()">
|
||||
{{ store.statusLoading ? '刷新中...' : '刷新状态' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="store.status">
|
||||
<div class="metrics">
|
||||
<div class="metric"><div class="label">自检</div><div class="value">{{ store.status.self_check }}</div></div>
|
||||
<div class="metric"><div class="label">网口1</div><div class="value">{{ store.status.net1 }}</div></div>
|
||||
<div class="metric"><div class="label">网口2</div><div class="value">{{ store.status.net2 }}</div></div>
|
||||
<div class="metric"><div class="label">串口1</div><div class="value">{{ store.status.uart1 }}</div></div>
|
||||
<div class="metric"><div class="label">串口2</div><div class="value">{{ store.status.uart2 }}</div></div>
|
||||
</div>
|
||||
<p class="hint">最后刷新:{{ store.statusUpdatedAt || '--' }}</p>
|
||||
</template>
|
||||
<p v-else>正在加载状态...</p>
|
||||
</section>
|
||||
</template>
|
||||
38
frontend/src/views/SystemConfigView.vue
Normal file
38
frontend/src/views/SystemConfigView.vue
Normal file
@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from "vue"
|
||||
import { saveSystemConfig } from "../api/platform"
|
||||
import { ensureSaveAuthorized } from "../utils/saveGuard"
|
||||
|
||||
defineProps<{ store: any; actions: any }>()
|
||||
|
||||
const form = reactive({
|
||||
time_sync: "auto",
|
||||
brightness: 80,
|
||||
screen_saver: 60,
|
||||
})
|
||||
const result = ref("未保存")
|
||||
|
||||
async function save() {
|
||||
const guard = await ensureSaveAuthorized()
|
||||
if (!guard.ok) {
|
||||
result.value = guard.message
|
||||
return
|
||||
}
|
||||
|
||||
const response = await saveSystemConfig(form)
|
||||
result.value = response.msg
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="panel">
|
||||
<h2>系统设置</h2>
|
||||
<div class="form-grid">
|
||||
<label>对时模式<select v-model="form.time_sync"><option value="auto">自动</option><option value="manual">手动</option></select></label>
|
||||
<label>亮度<input v-model="form.brightness" type="number" min="0" max="100" /></label>
|
||||
<label>屏保时间<input v-model="form.screen_saver" type="number" min="0" /></label>
|
||||
</div>
|
||||
<div class="actions"><button class="primary" @click="save">保存系统设置</button></div>
|
||||
<p>保存结果:{{ result }}</p>
|
||||
</section>
|
||||
</template>
|
||||
37
frontend/src/websocket/client.ts
Normal file
37
frontend/src/websocket/client.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import type { AlarmEvent, RealtimeData } from '../types/platform'
|
||||
|
||||
function buildWsUrl(path: string): string {
|
||||
return `ws://127.0.0.1:8000${path}`
|
||||
}
|
||||
|
||||
export function connectRealtime(onMessage: (data: RealtimeData) => void): WebSocket {
|
||||
const socket = new WebSocket(buildWsUrl('/ws/real-time'))
|
||||
socket.onmessage = (event) => {
|
||||
const payload = JSON.parse(event.data)
|
||||
onMessage(payload.data)
|
||||
}
|
||||
return socket
|
||||
}
|
||||
|
||||
export function connectRealtimeWithLifecycle(
|
||||
onMessage: (data: RealtimeData) => void,
|
||||
handlers: {
|
||||
onOpen?: () => void
|
||||
onClose?: () => void
|
||||
onError?: () => void
|
||||
},
|
||||
): WebSocket {
|
||||
const socket = connectRealtime(onMessage)
|
||||
socket.onopen = () => handlers.onOpen?.()
|
||||
socket.onclose = () => handlers.onClose?.()
|
||||
socket.onerror = () => handlers.onError?.()
|
||||
return socket
|
||||
}
|
||||
|
||||
export function connectAlarm(onMessage: (data: AlarmEvent) => void): WebSocket {
|
||||
const socket = new WebSocket(buildWsUrl('/ws/alarm'))
|
||||
socket.onmessage = (event) => {
|
||||
onMessage(JSON.parse(event.data))
|
||||
}
|
||||
return socket
|
||||
}
|
||||
15
frontend/tsconfig.json
Normal file
15
frontend/tsconfig.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"types": ["vite/client"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.vue"]
|
||||
}
|
||||
10
frontend/vite.config.ts
Normal file
10
frontend/vite.config.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 5173,
|
||||
},
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user