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

22 KiB
Raw Blame History

P3 模块自治架构设计(终态目标)

文档信息

  • 版本:V1.0(设计稿)
  • 日期:2026-08-14
  • 状态:设计评审中,未实施
  • 前提:新项目,数据可重置,不兼容旧架构;未来支持多机器人等共享资源
  • 背景:V0.6 全配置化流程引擎(internal/engine/)在生产暴露"全局耦合 + 机会主义触发"的结构性缺陷,连续发生多次全线死锁。本文档定义终态架构,历史缺陷清单见第 9 章(每个缺陷对应一条架构设计约束)。

目录

  1. 架构总览
  2. 核心设计原则
  3. 工件档案(Job as Data)
  4. 设备模块
  5. 搬运需求协议与预约制
  6. 共享资源服务(多机器人仲裁)
  7. 触发与轮询双机制
  8. 数据模型
  9. 历史缺陷清单与架构对策
  10. 实施路径

1. 架构总览

三层架构:工件档案层(数据)→ 设备模块层(状态机)→ 共享资源服务层(仲裁)。

┌──────────────────────────────────────────────────────────────┐
│ 工件档案层(Job as Data)                                      │
│   身份 / 位置 / 进度 / 质检结果 —— 只记录,不驱动,随工件流转被读写   │
└──────────────────────────────────────────────────────────────┘
        ▲ 工序完成事件                    │ 工序就绪请求(按配方查下一工序)
        │                                ▼
┌──────────────────────────────────────────────────────────────┐
│ 设备模块层(每设备 = 一个独立状态机,各自 触发+轮询 双机制)        │
│  ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│  │ 磨床模块  │ │ 清洗模块  │ │ 全检模块  │ │ 缓存模块  │ │ 接驳模块  │ │
│  │ 加工/换料 │ │ 吹洗     │ │ 批次检测 │ │ 缓冲/回流│ │ AGV交互  │ │
│  └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
└──────────────────────────────────────────────────────────────┘
        │ 搬运需求(装载→运输→卸载原子任务,目的地先预约)
        ▼
┌──────────────────────────────────────────────────────────────┐
│ 共享资源服务层(多机器人仲裁)                                    │
│   任务队列 + 优先级 + 目的地预约 + 机器人池匹配(未来可扩展任意共享资源)│
└──────────────────────────────────────────────────────────────┘

核心转变:工件(job)从"流程执行者"退化为"数据档案";"该发生什么"从"某个 job 恰好走到决策点"变为"设备模块状态机的转移条件 + 仲裁器的调度规则"。死锁在架构层面不可构造(见第 2 章)。


2. 核心设计原则

每条原则对应一个历史缺陷(详见第 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 表结构

-- 工件档案:身份 + 位置 + 进度(无步骤状态机)
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 通用模块模式

// 所有设备模块实现统一接口
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 需求结构(模块间唯一接口)

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 机器人池

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、夹具、检测仪等同构接入:

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 在任意时刻不处于"持料等待"状态(预约制)
  • 双机器人同时执行任务无冲突(槽位预约串行化)
  • 重启后从事件表完整恢复全部模块状态(无需人工干预)