From 656a2a6b361f31a01633abe916258fb2993d801e Mon Sep 17 00:00:00 2001 From: caoqianming Date: Thu, 26 Mar 2026 13:55:35 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20api=E6=96=87=E6=A1=A3=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- API.md | 739 +++++++++++++++++---------------------------------------- 1 file changed, 215 insertions(+), 524 deletions(-) diff --git a/API.md b/API.md index 9c2e9bf..104f432 100644 --- a/API.md +++ b/API.md @@ -1,17 +1,18 @@ -# PLC Control 接口说明 +# PLC Control API -本文档基于当前服务端路由与处理器代码整理,覆盖 HTTP API 和 WebSocket 实时消息。 +本文档基于当前后端代码整理,覆盖 HTTP API、SSE 日志流和 WebSocket 实时消息。 ## 基本信息 -- 服务端默认提供静态 UI:`/ui` -- HTTP API 前缀:`/api` -- 公共实时 WebSocket:`/ws/public` -- 客户端专属 WebSocket:`/ws/client/{client_id}` +- UI: `/ui` +- HTTP API 前缀: `/api` +- 公共 WebSocket: `/ws/public` +- 客户端专属 WebSocket: `/ws/client/{client_id}` +- 日志 SSE: `/api/logs/stream` ## 通用错误响应 -接口失败时通常返回: +失败时通常返回: ```json { @@ -21,103 +22,53 @@ } ``` -常见状态码: +常见状态码: -- `400 Bad Request`:参数错误 -- `403 Forbidden`:写入权限不足或控制条件不满足 -- `404 Not Found`:资源不存在 -- `500 Internal Server Error`:服务端内部错误 - ---- +- `400 Bad Request`: 参数非法或业务前置条件不满足 +- `403 Forbidden`: 控制条件不满足或无写入权限 +- `404 Not Found`: 资源不存在 +- `500 Internal Server Error`: 服务端内部错误 ## Source ### GET `/api/source` -获取所有已启用数据源及其连接状态。 - -响应示例: - -```json -[ - { - "id": "uuid", - "name": "PLC-1", - "protocol": "opcua", - "endpoint": "opc.tcp://127.0.0.1:4840", - "security_policy": null, - "security_mode": null, - "enabled": true, - "created_at": "2026-03-20 10:00:00.000", - "updated_at": "2026-03-20 10:00:00.000", - "is_connected": true, - "last_error": null, - "last_time": "2026-03-20 10:05:00.000" - } -] -``` +获取数据源列表及连接状态。 ### POST `/api/source` 创建数据源。 -请求体: +请求示例: ```json { "name": "PLC-1", + "protocol": "opcua", "endpoint": "opc.tcp://127.0.0.1:4840", "enabled": true } ``` -响应: - -```json -{ "id": "uuid" } -``` - ### PUT `/api/source/{source_id}` -更新数据源。请求体字段均可选: - -```json -{ - "name": "PLC-1", - "endpoint": "opc.tcp://127.0.0.1:4840", - "enabled": true, - "security_policy": "None", - "security_mode": "None", - "username": "user", - "password": "pass" -} -``` +更新数据源,字段均可选。 ### DELETE `/api/source/{source_id}` -删除数据源。成功响应:`204 No Content` +删除数据源。成功返回 `204 No Content`。 ### POST `/api/source/{source_id}/reconnect` -手动重连指定数据源。 - -```json -{ "ok_msg": "Source reconnected successfully" } -``` +手动重连数据源。 ### POST `/api/source/{source_id}/browse` -从 OPC UA 源浏览节点并写入本地 `node` 表。 - -```json -{ "ok_msg": "Browse completed", "total_nodes": 123 } -``` +从 OPC UA 数据源浏览节点并写入本地 `node` 表。 ### GET `/api/source/{source_id}/node-tree` -获取指定数据源的节点树(含 `children` 递归嵌套)。 - ---- +获取数据源节点树。 ## Point @@ -125,43 +76,12 @@ 分页获取点位列表,同时返回实时监测值。 -查询参数: +查询参数: -- `source_id`:可选,按数据源过滤 -- `equipment_id`:可选,按设备过滤 -- `page`:页码 -- `page_size`:每页条数(`-1` 表示全量) - -响应示例: - -```json -{ - "data": [ - { - "point": { - "id": "uuid", - "node_id": "uuid", - "name": "Temperature", - "equipment_id": "uuid", - "signal_role": "run", - "created_at": "2026-03-20 10:00:00.000", - "updated_at": "2026-03-20 10:00:00.000" - }, - "point_monitor": { - "point_id": "uuid", - "timestamp": "2026-03-20 10:05:00.000", - "quality": "good", - "value": 12.3, - "value_type": "float", - "value_text": "12.3" - } - } - ], - "total": 1, - "page": 1, - "page_size": 100 -} -``` +- `source_id` +- `equipment_id` +- `page` +- `page_size` ### GET `/api/point/{point_id}` @@ -169,70 +89,56 @@ ### GET `/api/point/{point_id}/history` -获取点位最近历史样本(进程内存环形缓冲,重启后清空)。 +获取最近历史样本。历史数据保存在进程内环形缓冲区,服务重启后清空。 -查询参数:`limit`(可选,默认 `120`,最大 `1000`) +查询参数: -```json -[ - { - "timestamp": "2026-03-20 10:05:00.000", - "quality": "good", - "value": 12.3, - "value_text": "12.3", - "value_number": 12.3 - } -] -``` +- `limit`: 默认 `120`,最大 `1000` ### PUT `/api/point/{point_id}` -更新点位元数据,字段均可选: +更新点位元数据。 + +可更新字段: ```json { "name": "Temperature", "description": "Room temperature", - "unit": "°C", + "unit": "C", + "tag_id": "uuid", "equipment_id": "uuid", "signal_role": "run" } ``` +说明: + +- 点位变更设备绑定或信号角色后,会唤醒相关控制单元任务,使控制引擎尽快使用最新映射。 + ### DELETE `/api/point/{point_id}` 删除单个点位。 +说明: + +- 删除后会同步通知相关控制单元刷新配置。 + ### POST `/api/point/batch` 根据节点批量创建点位。 -```json -{ "node_ids": ["uuid1", "uuid2"] } -``` - -响应: - -```json -{ - "success_count": 2, - "failed_count": 0, - "failed_node_ids": [], - "created_point_ids": ["uuid3", "uuid4"] -} -``` - ### DELETE `/api/point/batch` 批量删除点位。 -```json -{ "point_ids": ["uuid1", "uuid2"] } -``` +说明: + +- 删除后会同步通知相关控制单元刷新配置。 ### PUT `/api/point/batch/set-equipment` -批量设置点位的设备绑定和信号角色。 +批量设置点位设备绑定和信号角色。 ```json { @@ -242,28 +148,23 @@ } ``` +说明: + +- 更新前后关联到的控制单元都会被唤醒,避免控制引擎继续使用旧映射。 + ### PUT `/api/point/batch/set-tags` -批量设置点位的标签(`tag_id`,传 `null` 可清除绑定)。 - -```json -{ - "point_ids": ["uuid1", "uuid2"], - "tag_id": "uuid" -} -``` - -响应: - -```json -{ "ok_msg": "Point tags updated successfully", "updated_count": 2 } -``` +批量设置点位标签。 ### POST `/api/point/value/batch` 批量写点。 -请求头:`X-Write-Key: ` +请求头: + +- `X-Write-Key: ` + +请求示例: ```json { @@ -273,108 +174,64 @@ } ``` ---- - ## Equipment ### GET `/api/equipment` -分页获取设备列表,包含每台设备绑定的点位数量。 +分页获取设备列表,包含点位数量和已绑定信号角色点。 -查询参数:`page`、`page_size`(`-1` 全量)、`keyword`(可选,按 code/name 模糊搜索) +### GET `/api/equipment/{equipment_id}` -响应示例: +获取单个设备。 -```json -{ - "data": [ - { - "id": "uuid", - "unit_id": "uuid", - "code": "E01", - "name": "投煤器1", - "kind": "coal_feeder", - "description": null, - "created_at": "2026-03-20 10:00:00.000", - "updated_at": "2026-03-20 10:00:00.000", - "point_count": 5 - } - ], - "total": 1, - "page": 1, - "page_size": 20 -} -``` +### GET `/api/equipment/{equipment_id}/points` + +获取设备下所有点位。 ### POST `/api/equipment` 创建设备。 -```json -{ - "unit_id": "uuid", - "code": "E01", - "name": "投煤器1", - "kind": "coal_feeder", - "description": null -} -``` +说明: -响应:`201 Created` - -```json -{ "id": "uuid", "ok_msg": "Equipment created successfully" } -``` +- 如果设备绑定了控制单元,创建成功后会唤醒对应控制单元。 ### PUT `/api/equipment/{equipment_id}` -更新设备,字段均可选: +更新设备。 -```json -{ - "unit_id": "uuid", - "code": "E01", - "name": "投煤器1", - "kind": "coal_feeder", - "description": null -} -``` +说明: -### DELETE `/api/equipment/{equipment_id}` - -删除设备。成功响应:`204 No Content` - -### GET `/api/equipment/{equipment_id}/points` - -获取指定设备下所有绑定点位。 +- 如果设备更换了 `unit_id`、`kind` 或其他控制相关配置,旧单元和新单元都会被唤醒。 ### PUT `/api/equipment/batch/set-unit` -批量将设备绑定到控制单元。 +批量调整设备所属控制单元。 -```json -{ - "equipment_ids": ["uuid1", "uuid2"], - "unit_id": "uuid" -} -``` +说明: ---- +- 批量更新前关联到的旧单元,以及更新后关联到的新单元,都会收到唤醒通知。 -## Unit(控制单元) +### DELETE `/api/equipment/{equipment_id}` + +删除设备。成功返回 `204 No Content`。 + +说明: + +- 删除后会唤醒原所属控制单元。 + +## Unit ### GET `/api/unit` -分页获取控制单元列表。 - -查询参数:`page`、`page_size`、`keyword`(可选) - -响应字段包含:`id`、`code`、`name`、`enabled`、`run_time_sec`、`stop_time_sec`、`acc_time_sec`、`bl_time_sec`、`require_manual_ack_after_fault` +分页获取控制单元列表,返回单元基础信息、运行时快照和设备摘要。 ### POST `/api/unit` 创建控制单元。 +请求示例: + ```json { "code": "U01", @@ -389,29 +246,39 @@ } ``` -响应:`201 Created` +约束: -```json -{ "id": "uuid", "ok_msg": "Unit created successfully" } -``` - -### GET `/api/unit/{unit_id}` - -获取单个控制单元。 +- `run_time_sec > 0` +- `stop_time_sec > 0` +- `acc_time_sec > 0` +- `bl_time_sec > 0` +- `acc_time_sec > run_time_sec` ### PUT `/api/unit/{unit_id}` 更新控制单元,字段均可选。 +约束: + +- 如果更新后涉及时长字段,仍然必须满足上面的全部约束。 + ### DELETE `/api/unit/{unit_id}` -删除控制单元。成功响应:`204 No Content` +删除控制单元。成功返回 `204 No Content`。 + +### GET `/api/unit/{unit_id}` + +获取单个控制单元及其设备摘要。 + +### GET `/api/unit/{unit_id}/detail` + +获取控制单元完整详情,包括设备和点位列表。 ### GET `/api/unit/{unit_id}/runtime` -获取控制单元的当前运行时状态(内存中,不持久化)。 +获取控制单元当前运行时状态。 -响应示例: +响应字段: ```json { @@ -419,314 +286,178 @@ "state": "running", "auto_enabled": true, "accumulated_run_sec": 3600000, - "current_run_elapsed_sec": 60000, - "current_stop_elapsed_sec": 0, - "distributor_run_elapsed_sec": 0, + "display_acc_sec": 3600000, "fault_locked": false, "flt_active": false, "comm_locked": false, - "manual_ack_required": false, - "last_tick_at": "2026-03-25 10:00:00.000" + "manual_ack_required": false } ``` -`state` 枚举值:`stopped` / `running` / `distributor_running` / `fault_locked` / `comm_locked` +`state` 枚举: -注意:时间字段单位为毫秒(ms)。 +- `stopped` +- `running` +- `distributor_running` +- `fault_locked` +- `comm_locked` -### GET `/api/unit/{unit_id}/detail` +说明: -获取控制单元及其下所有设备和点位的完整嵌套结构。 +- `accumulated_run_sec` 和 `display_acc_sec` 单位都是毫秒。 +- 运行时状态保存在内存中,服务重启后重置。 -响应示例: - -```json -{ - "id": "uuid", - "code": "U01", - "name": "1号机组", - "enabled": true, - "equipments": [ - { - "id": "uuid", - "code": "E01", - "name": "投煤器1", - "kind": "coal_feeder", - "points": [ - { - "id": "uuid", - "name": "启动命令", - "signal_role": "start_cmd", - "equipment_id": "uuid" - } - ] - } - ] -} -``` - ---- - -## Event(系统事件) +## Event ### GET `/api/event` -分页获取系统控制事件记录。 +分页获取系统事件。 -查询参数: +查询参数: -- `unit_id`:可选,按控制单元过滤 -- `event_type`:可选,按事件类型过滤 -- `page`、`page_size` +- `unit_id` +- `event_type` +- `page` +- `page_size` -响应示例: +常见事件类型: -```json -{ - "data": [ - { - "id": "uuid", - "event_type": "equipment.start_command_sent", - "level": "info", - "unit_id": "uuid", - "equipment_id": "uuid", - "message": "Start command sent to equipment ...", - "payload": {}, - "created_at": "2026-03-25 10:00:00.000" - } - ], - "total": 1, - "page": 1, - "page_size": 20 -} -``` +- `unit.auto_control_started` +- `unit.auto_control_stopped` +- `unit.fault_locked` +- `unit.fault_acked` +- `unit.comm_locked` +- `unit.comm_recovered` +- `equipment.start_command_sent` +- `equipment.stop_command_sent` ---- +## Control -## Control(控制命令) +所有控制命令在执行前都会校验: -所有控制命令在执行前会校验:信号质量、REM 状态、FLT 状态、单元通讯锁、单元故障锁、人工确认状态。 +- 信号质量 +- REM 状态 +- FLT 状态 +- 单元 `auto_enabled` +- 单元 `comm_locked` +- 单元 `fault_locked` +- 单元 `manual_ack_required` ### POST `/api/control/equipment/{equipment_id}/start` -向设备发送启动脉冲命令(写 HIGH → 延迟 300ms → 写 LOW)。 - -响应示例: - -```json -{ - "ok_msg": "Equipment start command sent", - "equipment_id": "uuid", - "unit_id": "uuid", - "command_role": "start_cmd", - "command_point_id": "uuid", - "pulse_ms": 300 -} -``` - -失败(设备未处于可启动状态)返回 `403 Forbidden`。 +发送设备启动脉冲命令。 ### POST `/api/control/equipment/{equipment_id}/stop` -向设备发送停止脉冲命令。响应结构同上。 +发送设备停止脉冲命令。 ### POST `/api/control/unit/{unit_id}/start-auto` -启动指定控制单元的自动控制循环。 +启动单元自动控制。 -前置条件: -- 单元已启用(`enabled = true`) -- `fault_locked = false`(无活跃故障) -- `manual_ack_required = false`(故障已人工确认) +前置条件: + +- 单元已启用 +- `fault_locked = false` +- `comm_locked = false` +- `manual_ack_required = false` + +成功响应: ```json { "ok_msg": "Auto control started", "unit_id": "uuid" } ``` -失败时返回 `400 Bad Request`,`err_msg` 说明具体原因(故障锁定 / 待人工确认)。 - ### POST `/api/control/unit/{unit_id}/stop-auto` -停止自动控制循环。设备当前状态保持不变,不会自动停机。 - -```json -{ "ok_msg": "Auto control stopped", "unit_id": "uuid" } -``` +停止单元自动控制。 ### POST `/api/control/unit/batch-start-auto` -批量启动所有已启用(`enabled = true`)控制单元的自动控制。以下情况的单元将跳过:已在运行、`fault_locked`、`comm_locked`、`manual_ack_required`。 +批量启动所有已启用单元的自动控制。 -```json -{ - "started": ["uuid1", "uuid2"], - "skipped": ["uuid3"] -} -``` +会跳过以下单元: + +- 已经 `auto_enabled = true` +- `fault_locked = true` +- `comm_locked = true` +- `manual_ack_required = true` + +说明: + +- 单个启动和批量启动现在使用相同的阻断规则。 ### POST `/api/control/unit/batch-stop-auto` -批量停止所有自动控制中的控制单元。 - -```json -{ "stopped": ["uuid1", "uuid2"] } -``` +批量停止自动控制。 ### POST `/api/control/unit/{unit_id}/ack-fault` -人工确认故障,解除故障锁定。要求:`fault_locked = true` 且 `flt_active = false`(故障信号已消失)。 +人工确认故障。 -```json -{ "ok_msg": "Fault acknowledged", "unit_id": "uuid" } -``` +前置条件: ---- +- `fault_locked = true` +- `flt_active = false` -## Tag(标签) +## Tag ### GET `/api/tag` 分页获取标签列表。 -查询参数:`page`、`page_size` - -响应示例: - -```json -{ - "data": [ - { - "id": "uuid", - "name": "主蒸汽", - "description": null, - "created_at": "2026-03-20 10:00:00.000", - "updated_at": "2026-03-20 10:00:00.000" - } - ], - "total": 1, - "page": 1, - "page_size": 20 -} -``` - ### POST `/api/tag` -创建标签,可同时绑定点位。 - -```json -{ - "name": "主蒸汽", - "description": null, - "point_ids": ["uuid1", "uuid2"] -} -``` - -响应:`201 Created` - -```json -{ "id": "uuid", "ok_msg": "Tag created successfully" } -``` +创建标签。 ### GET `/api/tag/{tag_id}` -获取标签下所有绑定点位。 - -响应:点位对象数组。 +获取标签下已绑定点位。 ### PUT `/api/tag/{tag_id}` -更新标签,字段均可选(`point_ids` 传入时全量替换绑定关系): - -```json -{ - "name": "主蒸汽", - "description": "描述", - "point_ids": ["uuid1"] -} -``` +更新标签。 ### DELETE `/api/tag/{tag_id}` -删除标签。成功响应:`204 No Content` +删除标签。成功返回 `204 No Content`。 ---- - -## Page(自定义页面) +## Page ### GET `/api/page` -获取所有页面,按 `created_at` 排序。 - -查询参数:`name`(可选,模糊搜索) - -响应:Page 对象数组。 - -```json -[ - { - "id": "uuid", - "name": "总览", - "data": { "slot_key": "point-uuid" }, - "created_at": "2026-03-20 10:00:00.000", - "updated_at": "2026-03-20 10:00:00.000" - } -] -``` - -`data` 为 `{ slot_key: point_id }` 映射,用于页面布局与点位绑定。 +获取自定义页面列表。 ### POST `/api/page` 创建页面。 -```json -{ - "name": "总览", - "data": { "slot_a": "uuid1", "slot_b": "uuid2" } -} -``` - -响应:`201 Created` - -```json -{ "id": "uuid", "ok_msg": "Page created successfully" } -``` - ### GET `/api/page/{page_id}` 获取单个页面。 ### PUT `/api/page/{page_id}` -更新页面,字段均可选: - -```json -{ - "name": "总览", - "data": { "slot_a": "uuid1" } -} -``` +更新页面。 ### DELETE `/api/page/{page_id}` -删除页面。成功响应:`204 No Content` +删除页面。成功返回 `204 No Content`。 ---- - -## Log(运行日志) +## Log ### GET `/api/logs` -读取服务端日志文件内容(默认取最新 `app.log*` 文件尾部 200 行)。 +读取日志文件内容。默认读取最新的 `app.log*` 文件。 -查询参数: +查询参数: -- `file`:可选,指定文件名(仅允许 `app.log` 前缀) -- `cursor`:可选,上次返回的字节偏移量;传入时增量读取 cursor 之后的内容 -- `tail_lines`:可选,不传 cursor 时返回的尾部行数(默认 200,最大 2000) -- `max_bytes`:可选,单次最多返回字节数(默认 64 KB,最大 512 KB) +- `file`: 指定文件名,仅允许 `app.log*` +- `cursor`: 增量读取位置 +- `tail_lines`: 默认 `200`,最大 `2000` +- `max_bytes`: 默认 `64KB`,最大 `512KB` -响应示例: +响应示例: ```json { @@ -738,76 +469,51 @@ } ``` -- `truncated`:`true` 表示本次未读完,可用新 cursor 继续请求 -- `reset`:`true` 表示文件已轮转(cursor > 文件大小),已从头读取 +字段说明: + +- `truncated = true`: 当前还有未读完内容,可继续用新 `cursor` 拉取 +- `reset = true`: 文件被截断或读取起点被重置 ### GET `/api/logs/stream` -以 **SSE**(Server-Sent Events)流式推送日志增量,每 800 ms 检查一次文件变化。 +通过 SSE 推送日志增量。 -查询参数:`file`、`cursor`(可选,默认从文件末尾开始)、`max_bytes`(默认 32 KB) +查询参数: -事件格式: +- `file` +- `cursor` +- `max_bytes` -``` +默认行为: + +- 如果没有传 `file`,服务端会始终跟随最新的 `app.log*` +- 如果日志轮转到了新文件,流会自动切换到新文件,并推送一条 `reset = true` 的日志事件 + +事件示例: + +```text event: log -data: { "file": "app.log", "cursor": 204800, "lines": [...], "truncated": false, "reset": false } +data: { "file": "app.log.1", "cursor": 1024, "lines": ["..."], "truncated": false, "reset": true } event: error data: log stream read failed ``` ---- - ## WebSocket -### 连接地址 - -- 公共广播:`/ws/public` -- 客户端专属:`/ws/client/{client_id}` - -### 服务端主动推送消息 +### 服务端推送消息 #### `PointNewValue` -点位实时值更新: - -```json -{ - "type": "PointNewValue", - "data": { - "point_id": "uuid", - "timestamp": "2026-03-20 10:05:00.000", - "quality": "good", - "value": 12.3, - "value_type": "float", - "value_text": "12.3" - } -} -``` +点位实时值更新。 #### `EventCreated` -系统事件创建(控制操作、故障、状态变更等): - -```json -{ - "type": "EventCreated", - "data": { - "id": "uuid", - "event_type": "equipment.start_command_sent", - "level": "info", - "unit_id": "uuid", - "equipment_id": "uuid", - "message": "...", - "created_at": "2026-03-25 10:00:00.000" - } -} -``` +系统事件创建。 #### `UnitRuntimeChanged` -控制单元运行时状态变更(每 500ms tick 后广播): +控制单元运行时状态更新。 ```json { @@ -819,31 +525,19 @@ data: log stream read failed "fault_locked": false, "comm_locked": false, "manual_ack_required": false, - "accumulated_run_sec": 3600000 + "accumulated_run_sec": 3600000, + "display_acc_sec": 3600000 } } ``` #### `PointSetValueBatchResult` -批量写点结果回调: +批量写点结果回调。 -```json -{ - "type": "PointSetValueBatchResult", - "data": { - "success": true, - "err_msg": null, - "success_count": 1, - "failed_count": 0, - "results": [] - } -} -``` +### 客户端消息 -### 客户端发送消息 - -#### 写权限认证 +#### `auth_write` ```json { @@ -852,7 +546,7 @@ data: log stream read failed } ``` -#### 批量写点 +#### `point_set_value_batch` ```json { @@ -865,11 +559,8 @@ data: log stream read failed } ``` ---- - ## 备注 -- 运行时状态(`/runtime`)存储在内存中,服务重启后重置。 -- 历史曲线数据(`/history`)同样是内存环形缓冲,重启后清空。 -- 控制单元时间配置字段(`run_time_sec` 等)单位为秒,运行时 elapsed 字段单位为毫秒。 -- 自动控制启动后,状态机以 500ms 为周期运行,实时状态通过 WebSocket `UnitRuntimeChanged` 推送。 +- 控制引擎现在会在每轮单元循环中重新加载设备和角色映射,而不是只在任务启动时加载一次。 +- 设备和点位的控制相关配置变更后,会主动唤醒对应单元任务,使新配置尽快生效。 +- 日志流默认跟随最新日志文件,适配日志轮转场景。