Files
bj_power/bj_power_mes/docs/P3模块自治架构设计.md
T

398 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# P3 模块自治架构设计(终态目标)
## 文档信息
- **版本**:V1.0(设计稿)
- **日期**:2026-08-14
- **状态**:设计评审中,未实施
- **前提**:新项目,数据可重置,不兼容旧架构;未来支持多机器人等共享资源
- **背景**:V0.6 全配置化流程引擎(`internal/engine/`)在生产暴露"全局耦合 + 机会主义触发"的结构性缺陷,连续发生多次全线死锁。本文档定义终态架构,历史缺陷清单见[第 9 章](#9-历史缺陷清单与架构对策)(每个缺陷对应一条架构设计约束)。
---
## 目录
1. [架构总览](#1-架构总览)
2. [核心设计原则](#2-核心设计原则)
3. [工件档案(Job as Data)](#3-工件档案job-as-data)
4. [设备模块](#4-设备模块)
5. [搬运需求协议与预约制](#5-搬运需求协议与预约制)
6. [共享资源服务(多机器人仲裁)](#6-共享资源服务多机器人仲裁)
7. [触发与轮询双机制](#7-触发与轮询双机制)
8. [数据模型](#8-数据模型)
9. [历史缺陷清单与架构对策](#9-历史缺陷清单与架构对策)
10. [实施路径](#10-实施路径)
---
## 1. 架构总览
三层架构:**工件档案层(数据)→ 设备模块层(状态机)→ 共享资源服务层(仲裁)**。
```
┌──────────────────────────────────────────────────────────────┐
│ 工件档案层(Job as Data) │
│ 身份 / 位置 / 进度 / 质检结果 —— 只记录,不驱动,随工件流转被读写 │
└──────────────────────────────────────────────────────────────┘
▲ 工序完成事件 │ 工序就绪请求(按配方查下一工序)
│ ▼
┌──────────────────────────────────────────────────────────────┐
│ 设备模块层(每设备 = 一个独立状态机,各自 触发+轮询 双机制) │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 磨床模块 │ │ 清洗模块 │ │ 全检模块 │ │ 缓存模块 │ │ 接驳模块 │ │
│ │ 加工/换料 │ │ 吹洗 │ │ 批次检测 │ │ 缓冲/回流│ │ AGV交互 │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
└──────────────────────────────────────────────────────────────┘
│ 搬运需求(装载→运输→卸载原子任务,目的地先预约)
▼
┌──────────────────────────────────────────────────────────────┐
│ 共享资源服务层(多机器人仲裁) │
│ 任务队列 + 优先级 + 目的地预约 + 机器人池匹配(未来可扩展任意共享资源)│
└──────────────────────────────────────────────────────────────┘
```
**核心转变**:工件(job)从"流程执行者"退化为"数据档案";"该发生什么"从"某个 job 恰好走到决策点"变为"设备模块状态机的转移条件 + 仲裁器的调度规则"。死锁在架构层面不可构造(见[第 2 章](#2-核心设计原则))。
---
## 2. 核心设计原则
每条原则对应一个历史缺陷(详见[第 9 章](#9-历史缺陷清单与架构对策)):
| # | 原则 | 消灭的缺陷 |
|---|------|-----------|
| P1 | **预约制**:搬运任务派发前必须预约目的地槽位;预约失败 → 任务不启动(工件留原设备或改道缓存台) | 气吹持料死锁、换料持锁死锁 |
| P2 | **资源锁生命周期 = 任务执行期**:robot/temp_slot 锁只存在于"装载→运输→卸载"原子窗口,不存在于任何模块状态 | 换料后持锁进等待全线死锁 |
| P3 | **系统不变量 = 模块状态机一等转移条件**:满 2 件发 M2102、done 滞留强制取料等,是模块内部逻辑,不依赖任何外部 job 走到决策点 | 全检台满件 M2102 无人发 |
| P4 | **模块间只通过事件/数据通信**:不直接改对方状态、不共享锁;唯一共享的是资源服务(机器人) | 全局决策分支互相耦合、牵一发动全身 |
| P5 | **双机制兜底**:事件触发 + 轮询自检,事件丢失由自检兜底 | 广播无等待者、信号先到竞态 |
| P6 | **信号语义由模块协议定义**:工件类型/位置号是搬运协议的数据字段,不是可漏写的配置 | CWT 漏发、位置号地址复用混乱 |
---
## 3. 工件档案(Job as Data)
### 3.1 现状 vs P3
| | 现状(V0.6) | P3 |
|---|---|---|
| job 的角色 | 流程执行者:acquire 资源、发信号、决策跳转 | 数据档案:只记录,不驱动 |
| 步骤状态机 | `job_step` 快照表 + ACQUIRE→EXECUTE→WAIT→COMPLETE | 无步骤表,工序进度 = 追加事件 |
| 推进动力 | 引擎全局调度(GetActiveJobsOrdered 遍历) | 设备模块状态机 + 事件 |
| 工序完成 | job 自己推进 next_step | 模块写事件 → 档案更新 → 按配方请求下一工序 |
### 3.2 表结构
```sql
-- 工件档案:身份 + 位置 + 进度(无步骤状态机)
CREATE TABLE job (
id BIGSERIAL PRIMARY KEY,
work_order_id BIGINT NOT NULL, -- 工单
product_type_id BIGINT NOT NULL, -- 产品类型(CWT 数据源)
recipe_id BIGINT NOT NULL, -- 工艺路径(配方 = 数据)
step_index INT NOT NULL DEFAULT 0, -- 已推进到的工序序号(配方 steps 数组下标)
loc_equipment TEXT NOT NULL DEFAULT '', -- 当前位置:设备类型(GRINDER/WASH/FULL_CHECK/...)
loc_slot_no INT NOT NULL DEFAULT 0, -- 当前位置:槽位号
inspect_result TEXT, -- 质检结果
status TEXT NOT NULL DEFAULT 'CREATED', -- CREATED/IN_TRANSIT/AT_EQUIPMENT/COMPLETED/SCRAPPED
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 工序完成事件(append-only,进度真相源,可重放恢复)
CREATE TABLE job_event (
id BIGSERIAL PRIMARY KEY,
job_id BIGINT NOT NULL,
event_type TEXT NOT NULL, -- STEP_DONE / TRANSPORT_DONE / INSPECT_DONE / SCRAPPED ...
payload JSONB NOT NULL, -- {equipment:"GRINDER", slot:1, step:"GRIND", ...}
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 配方 = 工艺路径数据(不再有决策分支/资源/信号子表)
CREATE TABLE recipe (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
steps JSONB NOT NULL -- [{"step":"GRIND","equip":"GRINDER","params":{...}}, ...]
);
```
### 3.3 推进机制(事件驱动,幂等可重放)
```
设备模块完成工序
→ 写 job_event(STEP_DONE) + 更新 job.step_index / loc
→ 按 recipe.steps[step_index+1] 查下一工序 → 请求目标设备模块(投递其事件队列)
→ 目标模块评估(有空位?可接收?)
→ 有 → 产生搬运需求(投入仲裁队列)
→ 无 → 请求挂起(工件留在原设备,不占任何锁;模块自检在有空位时重新评估)
```
- **幂等**:job_event 是进度真相源,重启后从事件表重建各模块状态,无需步骤快照
- **工序间解耦**:每个模块只关心"我这里有活干吗",不关心全局下一个是谁
---
## 4. 设备模块
### 4.1 通用模块模式
```go
// 所有设备模块实现统一接口
type EquipmentModule interface {
OnEvent(ctx, evt ModuleEvent) error // 事件驱动:信号/搬运完成/工序请求
SelfCheck(ctx) error // 轮询兜底:超时/滞留/满仓检测
ReserveSlot(ctx, jobID, kind) (SlotRef, error) // 目的地预约(供仲裁器调用,P1 预约制)
}
```
模块内部状态机通用骨架:
```
IDLE → OCCUPIED(工序执行中)→ DONE(完成待搬运)→ IDLE
↑ │
└──────── 搬运完成 ──────────┘
```
- **状态只存在于模块内存 + 本模块的表**,其他模块不可见不可改(P4)
- 每个模块自带:事件队列(串行处理,天然防竞态)+ 自检 tick(P5)
### 4.2 磨床模块
```
状态:IDLE / 加工中(GRINDING) / 完成滞留(DONE) / 换料中(EXCHANGING)
事件:放料完成(M2049) → GRINDING;加工完成(M2057) → DONE → 产生搬运需求
后继毛坯到达(搬运任务到达)→ EXCHANGE 任务(一次取成品+放毛坯)
自检:DONE 滞留超时 → 强制产生取料需求(磨床空出,不无限等后继)
规则:加工完成不"等后继换料"挂起——超时即取料收尾,磨床优先空出(对应 V0.6 handleHolding 逻辑内聚)
```
### 4.3 清洗模块(气吹机构)
```
状态:IDLE / 吹洗中(WASHING) / 完成待搬运(DONE)
事件:放料完成(M2139?) → WASHING;吹洗完成 → DONE
规则(修正持料模型,P1/P2):
- 吹洗完成瞬间 → 先预约目的地(全检台空位 / 缓存台空位 / 全检 done 槽换料位)
- 预约成功 → 派发搬运任务,机器人从气吹机构直接 carry 到目的地(物理行为与现状一致)
- 预约失败 → 工件留在气吹机构(不交给 robot),模块自检持续重试预约;
若全检台满 + 缓存台满 → 系统满仓告警(物理无解,人工介入)
自检:WASHING 超时 → 告警;DONE 预约超时 → 告警 + 重试
```
**对比现状(缺陷 1)**:现状是"机器人夹着工件(持锁)再找地方放",全检台满了 robot 就死等;P3 是"先确认地方再拿 robot",robot 永远不会持料等待。
### 4.4 全检模块(批次检测内聚)
```
槽位对状态:(S1,S2)
EMPTY/EMPTY → 放料 → OCCUPIED/EMPTY → 放料 → OCCUPIED/OCCUPIED
→ 【满 2 件自动发 M2102(模块一等转移条件,P3 原则)】→ INSPECTING(等 M2098)
→ DONE/DONE → 产生取料搬运需求(每 done 槽一件)/ 换料任务(后继待检件到达时)
→ EMPTY/EMPTY
事件:放料完成(M2109)、检测完成(M2098 广播)、搬运完成
自检:INSPECTING 超时 → 告警(检测卡死);DONE 滞留超时 → 强制取料需求
规则:
- 换料:有 done 槽 + 待检件到达 → EXCHANGE 任务(一次取 done + 放待检,对应 OP40_EXCHANGE)
- 批次检测逻辑(满 2 件发 M2102、广播唤醒、收尾批次)全部内聚本模块,不依赖任何 job
- M2102 防重复:状态机只在 OCCUPIED/OCCUPIED → INSPECTING 转移时发一次
```
### 4.5 缓存模块(缓冲队列)
```
状态:槽位 空/占(N 个槽)
角色:目的地不可用时的改道目标(P1);回流源(全检台有空位时产生回流搬运需求)
事件:放料完成、取料完成
自检:占用超时(无回流需求产生)→ 告警
规则:全检台有空位 + 缓存台有件 → 自动产生回流搬运需求(对应 V0.6 缓存回流内聚)
```
### 4.6 接驳台模块(AGV 交互)
```
角色:槽位统一分配器(消灭缺陷 8:"job 未绑定槽位回退任意槽位")
放料(AGV 来料)/ 取料(成品下料回 AGV)
事件:AGV 到位、放料完成(M2019)、取料完成(M2009)
规则:槽位分配 = 模块内部数据,不散落在 job 行
```
---
## 5. 搬运需求协议与预约制
### 5.1 需求结构(模块间唯一接口)
```go
type TransportRequest struct {
ID string
JobID int
From SlotRef // 源:设备类型 + 槽位号
To SlotRef // 目标:预约时确定
Kind Kind // MOVE(普通搬运)/ EXCHANGE(换料:取+放一次)/ REDIRECT(改道)
Priority int // EXCHANGE > 紧急取料 > 普通搬运
State State // QUEUED → RESERVED → EXECUTING → DONE
// ↘ REDIRECTED(改道缓存台)→ DONE
// ↘ CANCELLED(源槽位状态变化)
}
```
### 5.2 任务生命周期(预约制核心,P1)
```
1. 源模块产生需求 → QUEUED(不占任何锁、不占 robot)
2. 仲裁器按优先级选任务 → 调目标模块 ReserveSlot()(事务内锁定目标槽位)
├─ 成功 → RESERVED → 派发 robot → EXECUTING
│ robot 执行"装载→运输→卸载"原子窗口(P2:锁只存在于此窗口)
│ → 完成事件 → DONE → 通知源/目标模块 + 更新 job 档案
├─ 失败(目标满)→ 改道:REDIRECTED(目标=缓存台,若有空位)
│ └─ 缓存台也满 → 任务挂起(源模块自检重试预约,P5)
└─ 源槽位状态变化(工件被其他任务取走)→ CANCELLED
```
**防死锁不变量**:
- robot 派发时目标已预约锁定,物理上不可能出现"到了没地方放"
- 源模块产生需求不持锁,任务挂起不占 robot
- 唯一可能的僵局 = 系统满仓(全检满 + 缓存满 + 无取料动作)→ 告警,物理无解只能人工
### 5.3 多段搬运优化(对应现状"持料 carry 连续多段")
清洗 → 全检 → 下料一次连续搬运(效率优化):**预约链**——连续段的目标全部预约成功才允许启动;任一段预约失败 → 中断在安全点(工件落缓存台),不持料等待。
---
## 6. 共享资源服务(多机器人仲裁)
### 6.1 机器人池
```sql
CREATE TABLE robot (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
zone TEXT, -- 工作区域(就近调度用)
status TEXT NOT NULL DEFAULT 'IDLE', -- IDLE/BUSY/MAINTENANCE
current_task_id TEXT,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```
### 6.2 仲裁器(Robot Dispatcher)
```
每 tick / 任务事件:
1. 从队列取最高优先级任务(同优先级 FIFO)
2. 目标模块 ReserveSlot()(失败 → 改道/挂起,见 5.2)
3. 匹配空闲机器人:zone 就近优先 → 轮询均衡
4. 派发 → robot.BUSY + task.EXECUTING → 完成事件 → robot.IDLE
5. 自检:任务 EXECUTING 超时 → 告警(机器人卡死/物理故障)
```
### 6.3 多机器人并行安全
- **目的地槽位预约串行化**:同一槽位同一时刻只被一个任务预约(事务锁),两个 robot 不会抢同一目标
- **源槽位状态锁定**:任务 RESERVED 后源槽位标记,防止两个任务重复取同一工件
- robot 之间无共享状态,天然并行
### 6.4 扩展:任意共享资源
机器人只是共享资源的一种。未来 AGV、夹具、检测仪等同构接入:
```go
type SharedResource interface {
Acquire(ctx, taskID) (bool, error) // 预约/占用
Release(ctx, taskID) error
}
```
调度规则统一:任务 → 资源预约(成功才执行)→ 执行 → 释放。预约制在所有共享资源上复用,死锁不可构造。
---
## 7. 触发与轮询双机制
每个模块独立运行两条线(P5):
```
触发线(即时):PLC 信号上升沿 → 模块事件队列(串行)→ 状态机转移
轮询线(兜底):模块自检 tick(如 30s)→ 超时/滞留/满仓/任务卡死检测 → 自动动作或告警
```
| 自检项 | 检测 | 动作 |
|---|---|---|
| 工序超时 | 模块状态停留超阈值 | 告警 |
| 滞留 | DONE 槽位超时无搬运需求 | 自动产生取料需求(磨床/全检台) |
| 满仓 | 全检满 + 缓存满 + 无取料 | 告警(人工介入) |
| 检测卡死 | INSPECTING 超时 | 告警(重发 M2102 或人工) |
| 任务卡死 | EXECUTING 超时 | 告警(机器人故障) |
| 预约失败循环 | 挂起任务超时 | 告警 |
事件丢了 → 自检兜底;自检慢了 → 事件即时。**不存在"信号没人等"的盲区**(对比缺陷 6/7:广播无等待者、信号先到竞态——事件队列 + 自检天然覆盖)。
---
## 8. 数据模型
```
job 工件档案(身份/位置/进度,见 3.2)
job_event 工序完成事件(append-only 真相源)
recipe 工艺路径(steps JSONB)
work_order 工单
product_type 产品类型
equipment / equipment_slot / equipment_type 设备与槽位(模块私有数据,但库表统一)
robot 机器人池
transport_task 搬运任务表(仲裁队列持久化,重启可恢复)
module_state 模块状态快照(可选:重启恢复加速,非真相源)
event_log / alarm 事件与报警(管理后台)
```
**重启恢复**:从 job_event + transport_task 重建全部模块状态(事件可重放),无步骤快照一致性难题。
---
## 9. 历史缺陷清单与架构对策
> 本节为 V0.6 及更早版本在生产中暴露的全部关键缺陷。**每条缺陷都对应第 2 章的一条设计原则**——P3 架构评审时逐条核对,确保不再重犯。
| # | 缺陷 | 现象 | 根因 | 架构对策(P3) |
|---|------|------|------|----------------|
| 1 | **气吹机构持料死锁** | 机器人夹持工件在气吹机构吹洗(无放料位置,M2139 完成 robot 不释放),carry 去下一步;下一步不可用(全检台满)→ robot 被持死,全线瘫痪(2026-08-14 job6 持 robot 卡 OP35_DECIDE 16 分钟) | 持料模型:"先拿 robot 锁,再找目的地";目的地满则死等 | **P1 预约制**:先预约目的地再派 robot;预约失败任务不启动。清洗模块 DONE 状态持续重试预约,robot 永不持料等待 |
| 2 | **换料后持锁进等待死锁** | 换料完成后持前任成品 + robot 锁进入决策/等待 → 锁死全线(2026-08-13 线上 M2089 后全线卡死事故;配方注释明确"禁止持料进决策") | 资源锁生命周期 = job 步骤生命周期,锁随 job 进等待步骤 | **P2 锁生命周期 = 任务执行期**:模块状态机不持有全局锁;换料是一个原子 EXCHANGE 任务,完成后锁即释放 |
| 3 | **全检台满 2 件 M2102 无人发** | 全检台 2 槽满待检件,但无任何 job 到批次决策点(换料路径被缓存回流分支拐走)→ 检测不开始 → 全线死锁(2026-08-14,16:12-16:22) | 系统不变量由 job 流程"机会主义"触发,路径一绕路就没人管 | **P3 模块一等转移条件**:满 2 件发 M2102 是全检模块状态机内部转移,与 job 流程完全无关 |
| 4 | **决策回退无限空转** | 决策跳转 → acquire 失败 → 回退决策 → 条件未变 → 同一分支 → 循环(OP61_DECIDE↔OP50_EXCHANGE_TAKE,每事件 4 个 DB 事务 + 日志刷屏) | 决策-跳转-失败-回退机制无防循环保护 | 架构消灭:无全局决策步骤,无决策跳转,无回退 |
| 5 | **GROUP BY 缺失调度崩溃** | successorCanExchange SQL 缺 GROUP BY → SQLSTATE 42803 → 引擎推进失败 → 全线卡死 | SQL 缺陷(依赖全局 job 关联查询) | 无此 SQL:模块间不查对方状态 |
| 6 | **广播信号无等待者** | M2098 广播上升沿无任何 job 在等("缓存未匹配"×2)→ 检测结果无人消费,死锁前兆 | 广播依赖"恰好有 job 注册等待",时序竞态 | **P5 双机制**:模块状态机事件队列 + 自检兜底,不依赖瞬时注册 |
| 7 | **Done 信号先到竞态** | 信号到达时步骤尚未进入 waiting_done → 信号丢失(pendingSignals 缓存 + 5s 时效校验是补丁) | 信号消费依赖步骤状态瞬时匹配 | 模块事件队列串行处理 + 状态机自检(P5) |
| 8 | **接驳台槽位未绑定回退任意** | job 未绑定接驳台槽位时 acquire 回退"任意槽位"→ 位置语义错乱风险(日志 WARN 高频出现) | 槽位分配分散在 job 行内字段,无统一管理 | 接驳台模块统一分配槽位(P4 模块私有数据) |
| 9 | **CWT 工件类型漏发** | 每组信号都应携带 M2022 工件类型,历史出现漏发(用户生产反馈) | 信号序列人为配置,工件类型是"可选的配置项" | 工件类型是 job 档案的必填字段,搬运协议强制携带(P6) |
| 10 | **位置号地址复用混乱** | M2082(换料放位置)与 M2101(放料位置)共用 PLC 地址,语义靠配方 desc 注释区分 | 信号地址语义散落在配方描述 | 信号语义由模块协议定义,地址绑定在模块内(P6) |
**附加经验(非缺陷,但影响设计)**:
- 持料模型是生产科室物理规则(气吹机构无放料位置),P3 保留物理行为,仅改软件时序(先预约后派发)
- 磨床"断料才磨完即走、后继能取料则等换料交接":内聚为磨床模块自检规则(DONE 滞留超时强制取料)
- recipe 表历史字段坑(无 is_del,用 is_active):新架构 recipe 表重新设计,无此问题
- 全量测试会卡死生产:P3 模块化后单元测试按模块隔离,天然可并行
---
## 10. 实施路径
前提:新项目数据可重置。两条路:
**路线 A(推荐):直接按 P3 落地**(新项目无历史包袱)
1. 数据模型 + 事件系统(job_event / transport_task / 模块状态机骨架)
2. 设备模块 × 5(磨床/清洗/全检/缓存/接驳)+ 信号路由改造
3. 仲裁器(单机器人先跑通,接口按多机器人设计)
4. 前端看板 + 人工交互对接
**路线 B:现有代码渐进改造**(若仍需复用 V0.6 资产)
- P0 止血:通用监控框架(full_slot_request / resource_hold_timeout / orphan_broadcast 三条规则)
- P1 模块化封装:设备状态管理拆模块,引擎降级为 job 推进 + 事件路由
- P2 仲裁重构:搬运任务队列 + 目的地预约(修正持料模型)
- P3 终态:模块自治,job 退化为数据
**阶段验收标准(P3)**:
- [ ] 任一设备模块故障/卡死,其他模块继续运转(故障隔离)
- [ ] 全检台满 2 件 100% 自动发 M2102(不依赖任何 job 路径)
- [ ] robot 在任意时刻不处于"持料等待"状态(预约制)
- [ ] 双机器人同时执行任务无冲突(槽位预约串行化)
- [ ] 重启后从事件表完整恢复全部模块状态(无需人工干预)