705 lines
28 KiB
Markdown
705 lines
28 KiB
Markdown
# 工业自动化生产线管控系统 — 详细设计方案
|
||
|
||
## 文档信息
|
||
- **版本**:V0.6(全配置化流程引擎)
|
||
- **日期**:2026-08-10
|
||
- **状态**:与代码同步
|
||
- **说明**:新增全配置化流程引擎(`internal/engine/`),引擎作为纯执行器,所有业务逻辑通过数据库配置驱动。详细设计见 `流程文档.md`。
|
||
|
||
---
|
||
|
||
## 目录
|
||
1. [系统架构总览](#1-系统架构总览)
|
||
2. [核心组件职责](#2-核心组件职责)
|
||
3. [启动流程](#3-启动流程)
|
||
4. [Actor 模型](#4-actor-模型)
|
||
5. [工艺配方系统](#5-工艺配方系统)
|
||
6. [信号路由系统](#6-信号路由系统)
|
||
7. [事件系统](#7-事件系统)
|
||
8. [调度器](#8-调度器)
|
||
9. [数据流与写入路径](#9-数据流与写入路径)
|
||
10. [断电恢复](#10-断电恢复)
|
||
11. [数据库设计](#11-数据库设计)
|
||
12. [PLC 信号映射](#12-plc-信号映射)
|
||
13. [关键技术选型](#13-关键技术选型)
|
||
|
||
---
|
||
|
||
## 1. 系统架构总览
|
||
|
||
本系统是一个**单体应用**,内部按职责分层模块化,打包为单个 Go 可执行文件。
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────┐
|
||
│ 人工终端 (React) │
|
||
└──────────────────────┬───────────────────────────────────┘
|
||
│ SSE / HTTP
|
||
┌──────────────────────▼───────────────────────────────────┐
|
||
│ go-zero HTTP Server (API层) │
|
||
│ ┌──────────┐ ┌───────────┐ ┌──────────────────────┐ │
|
||
│ │ 工单管理 │ │ 看板/监控 │ │ 人工交互(暂停/恢复)│ │
|
||
│ └────┬─────┘ └─────┬─────┘ └──────────┬───────────┘ │
|
||
│ │ │ │ │
|
||
│ ┌────▼──────────────▼───────────────────▼────────────┐ │
|
||
│ │ 业务逻辑层 │ │
|
||
│ │ ┌────────────┐ ┌───────────┐ ┌──────────────┐ │ │
|
||
│ │ │ 工单处理器 │ │ 事件循环 │ │ 调度器 │ │ │
|
||
│ │ │ (processor) │ │(eventloop)│ │ (scheduler) │ │ │
|
||
│ │ └────────────┘ └───────────┘ └──────────────┘ │ │
|
||
│ └────────────────────────┬──────────────────────────┘ │
|
||
│ │ │
|
||
│ ┌────────────────────────▼──────────────────────────┐ │
|
||
│ │ 基础设施层 │ │
|
||
│ │ ┌──────────┐ ┌──────────┐ ┌────────────────┐ │ │
|
||
│ │ │ 事件总线 │ │ PLC 管理 │ │ 机器人管理 │ │ │
|
||
│ │ │(eventbus)│ │(plc) │ │(robot) │ │ │
|
||
│ │ └──────────┘ └──────────┘ └────────────────┘ │ │
|
||
│ │ ┌──────────┐ ┌──────────┐ ┌────────────────┐ │ │
|
||
│ │ │ 设备代理 │ │ 信号路由 │ │ SSE 推送 │ │ │
|
||
│ │ │ (actor) │ │(signal) │ │(sse) │ │ │
|
||
│ │ └──────────┘ └──────────┘ └────────────────┘ │ │
|
||
│ └───────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ PostgreSQL (持久化) │
|
||
└──────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 分层职责
|
||
|
||
| 层 | 职责 | 关键约束 |
|
||
|----|------|---------|
|
||
| **API 层** | HTTP 入口,请求校验,响应组装 | 不写业务逻辑 |
|
||
| **业务逻辑层** | 工单管理、调度决策、事件处理 | 核心状态机,所有决策在此 |
|
||
| **基础设施层** | 事件总线、PLC 通信、机器人控制 | 可替换实现,不包含业务逻辑 |
|
||
| **设备代理层** | Actor 管理槽位状态,处理设备信号 | 不写数据库,不调度 |
|
||
|
||
---
|
||
|
||
## 2. 核心组件职责
|
||
|
||
### 2.1 事件循环 (EventLoop) — 产线大脑
|
||
|
||
**文件位置:** `internal/processor/eventloop/`
|
||
|
||
**职责:**
|
||
- 处理所有生产事件:PLC 信号、调度结果、前端指令
|
||
- 驱动工件状态迁移(Job 状态机)
|
||
- 协调调度器下发任务
|
||
- 统一写入数据库(唯一写 DB 的入口)
|
||
|
||
**启动方式:** 独立 goroutine,`go eventLoop.Run(ctx)`
|
||
|
||
**关键行为:**
|
||
- 接收 PLC 信号 → 更新对应 Actor 槽位状态
|
||
- 触发调度器 → 决策下一步动作
|
||
- 下发动作指令 → PlcWorker 执行
|
||
- 处理执行结果 → 更新 Job 状态
|
||
|
||
### 2.2 工单处理器 (JobProcessor) — 订单管家
|
||
|
||
**文件位置:** `internal/processor/`
|
||
|
||
**职责:**
|
||
- 工单创建(校验料位、生成 Job)
|
||
- 工单暂停/恢复
|
||
- 暂存台补料策略触发
|
||
- 工单完成统计
|
||
|
||
### 2.3 调度器 (Scheduler) — 决策者
|
||
|
||
**文件位置:** `internal/processor/scheduler/`
|
||
|
||
**职责:**
|
||
- 从就绪工件中选择下一个要处理的
|
||
- 选择目标设备(约束过滤 + 策略排序)
|
||
- 生成搬运任务(取→放)
|
||
|
||
**三层架构:**
|
||
1. **任务生成层** — 根据工件当前步骤生成候选动作
|
||
2. **约束过滤层** — 排除不可用设备、暂停工单、冲突等
|
||
3. **策略选择层** — 按优先级排序
|
||
|
||
### 2.4 Actor — 设备代理
|
||
|
||
**文件位置:** `internal/processor/actor/`
|
||
|
||
**职责:**
|
||
- 管理设备槽位状态(占用/空闲)
|
||
- 处理 PLC 发来的设备信号
|
||
- 不写数据库,不调度
|
||
|
||
### 2.5 信号路由 (SignalRouter) — 信号分拣
|
||
|
||
**文件位置:** `internal/processor/actor/`
|
||
|
||
**职责:**
|
||
- 每秒轮询 PLC 输入寄存器
|
||
- 检测信号变化(上升沿/下降沿)
|
||
- 根据信号找到对应 Actor,分发处理
|
||
|
||
### 2.6 PLC 管理器 (PLC Manager) — 硬件通信
|
||
|
||
**文件位置:** `internal/plc/`
|
||
|
||
**职责:**
|
||
- 建立与西门子 S1200 的通信连接
|
||
- 读写 PLC 寄存器(Modbus TCP / OPC-UA)
|
||
- 发送动作指令(取料、放料、换料等)
|
||
|
||
### 2.7 事件总线 (EventBus) — 消息通道
|
||
|
||
**文件位置:** `internal/eventbus/`
|
||
|
||
**职责:**
|
||
- 组件间解耦通信
|
||
- 当前实现:`LocalBus`(内存 channel)
|
||
|
||
### 2.8 机器人管理器 (Robot Manager) — 搬运执行
|
||
|
||
**文件位置:** `internal/robot/`
|
||
|
||
**职责:**
|
||
- 控制地轨机器人执行取/放动作
|
||
- 管理手持工具(扫码枪、激光打标机)
|
||
- 任务执行结果回传
|
||
|
||
---
|
||
|
||
## 3. 启动流程
|
||
|
||
启动顺序分三个阶段,按依赖关系排列:
|
||
|
||
### 阶段一:基础设施初始化
|
||
|
||
```
|
||
1. PostgreSQL 连接 → ent ORM + 原生 SQL 可用
|
||
2. 预加载 → 信号地址表 + 产品类型加载到内存
|
||
3. 事件总线创建 → 组件间通信通道(此时未启动,防止事件丢失)
|
||
4. 告警服务注册 → 订阅异常事件
|
||
6. PLC 管理器创建 → 硬件通信通道建立
|
||
7. 机器人管理器创建 → 上下料能力就绪
|
||
```
|
||
|
||
### 阶段二:业务组件组装
|
||
|
||
```
|
||
8. 工艺配方加载 → 各工件型号的加工步骤序列可用
|
||
9. 调度器创建 → 决策策略就绪
|
||
10. 产线 Actor 构建 → 每个设备对应一个 Actor 管理槽位
|
||
11. 工单处理器创建 → 可管理工单全生命周期
|
||
12. PLC 动作执行器创建 → 可将动作指令转为 PLC 信号
|
||
13. 事件循环创建 → 产线状态机核心(此时未运行)
|
||
14. 信号路由绑定事件循环 → PLC 信号可直接驱动状态机
|
||
15. 信号轮询启动(goroutine)→ 每秒读取 PLC 信号
|
||
```
|
||
|
||
### 阶段三:启动与恢复
|
||
|
||
```
|
||
16. 事件总线启动 → 之前注册的订阅开始生效
|
||
17. 事件日志写入器启动 → 所有事件异步写入数据库
|
||
18. 总线全量订阅 → 日志写入 → 审计追溯能力就绪
|
||
19. SSE 桥接器创建 → 事件实时推送到前端
|
||
20. 断电恢复 → 从数据库读取未完成工单
|
||
├─ L1 自动恢复 → CREATED/ON_BUFFER 直接恢复
|
||
├─ L2 半自动恢复 → PROCESSING 且 PLC 确认完成
|
||
└─ L3 人工确认 → 发布事件通知前端
|
||
21. Actor 槽位同步 → 内存状态与 DB 一致
|
||
22. 事件循环启动(goroutine)→ 产线状态机开始运行
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Actor 模型
|
||
|
||
### 4.1 什么是 Actor
|
||
|
||
每个 Actor 对应一台物理设备,管理该设备的所有槽位。Actor 是**统一实现**(`MachineActor`),通过 `Type` 字段适配不同设备类型。Actor 不调度、不写数据库,只做两件事:
|
||
- 维护槽位状态(占用/空闲/哪个工件在上面)
|
||
- 处理 PLC 发来的信号(加工完成 → 把槽位状态改为 DONE)
|
||
|
||
Actor 是 mutex 保护的同步状态结构体,由 EventLoop 同步调用,不启动独立 goroutine。
|
||
|
||
### 4.2 设备类型与槽位配置
|
||
|
||
| 设备类型 | 槽位数 | 支持 Exchange | 说明 |
|
||
|----------|--------|-------------|------|
|
||
| GRINDER | 1 | 是 | 单槽位,支持换料(取成品+放毛坯一次完成) |
|
||
| TURN_TABLE | 1 | 是 | 单槽位,支持换料,用于工件换向 |
|
||
| CACHE | 1 | 否 | 单槽位暂存缓冲,仅由槽位占用状态表达语义 |
|
||
| FULL_CHECK | 2 | 否 | 双槽位检测台,OK/NG 双结果信号 |
|
||
| AIR_BLOW | 1 | 否 | 单槽位,气吹清洁 |
|
||
| DOCK | 84 | 否 | 不建模为 MachineActor,由 PlcExecutor 直接驱动 |
|
||
|
||
### 4.3 Actor 槽位状态机
|
||
|
||
每个 Actor 的槽位经历以下状态:
|
||
```
|
||
EMPTY → OCCUPIED → DONE → EMPTY
|
||
```
|
||
|
||
- `EMPTY`:空闲,可放工件
|
||
- `OCCUPIED`:有工件,设备正在加工/处理中
|
||
- `DONE`:处理完成,等待机器人取走
|
||
|
||
### 4.4 Actor 与 PlcExecutor 的关系
|
||
|
||
Actor 只管槽位状态(纯内存),PlcExecutor 管 PLC 信号握手。两者分离:
|
||
- SignalRouter 检测到 PLC 完成信号 → 投递 SignalEvent 到 EventLoop
|
||
- EventLoop.handleMachineSignal → Actor.MarkSlotDone → 更新槽位状态
|
||
- 调度器决定动作 → PlcWorker 直接调用 PlcExecutor 方法执行 PLC 握手
|
||
|
||
---
|
||
|
||
## 5. 工艺配方系统
|
||
|
||
### 5.1 配方结构
|
||
|
||
每种工件型号(Product Type)对应一份 Recipe(工艺配方),Recipe 包含一组有序的 Step(工序步骤)。
|
||
|
||
**Recipe 表:**
|
||
```
|
||
recipe {
|
||
id // 配方编号,如 "R-A6VM107"
|
||
name // 名称
|
||
version // 版本号
|
||
}
|
||
```
|
||
|
||
**RecipeStep 表:**
|
||
```
|
||
recipe_step {
|
||
step_id // 步骤编号,如 "10", "20"
|
||
recipe_id // 所属配方
|
||
step_name // 步骤名称
|
||
step_index // 步骤序号
|
||
step_type // 工序类型(见下表)
|
||
resource_type // 设备资源类型
|
||
next_step_default // 默认下一步
|
||
desc // 步骤描述
|
||
}
|
||
```
|
||
|
||
### 5.2 工序类型
|
||
|
||
工序类型(`step_type`)由 `agent业务.md` 定义,不同项目可不同。框架支持以下通用工序类型:
|
||
|
||
| 工序类型 | 含义 | 需要什么资源 |
|
||
|----------|------|-------------|
|
||
| LOAD | 上料并扫码 | 搬运设备 + 接驳台 |
|
||
| BUFFER_STAGE | 暂存台等待 | 暂存台槽位 |
|
||
| MACHINING | 加工 | 加工设备 |
|
||
| INSPECTION | 检测 | 检测设备 |
|
||
| PLACE | 下料 | 搬运设备 + 接驳台 |
|
||
|
||
具体工序类型和步骤内容见 `agent业务.md` 和 `doc/data.sql`。
|
||
|
||
### 5.3 配方加载时机
|
||
|
||
系统启动时从数据库加载所有 Recipe 到内存(`RecipeLoader`),运行期间不查库。新增配方需要重启或手动刷新。
|
||
|
||
---
|
||
|
||
## 6. 信号路由系统
|
||
|
||
### 6.1 信号流向
|
||
|
||
```
|
||
PLC 输入寄存器
|
||
│
|
||
▼
|
||
SignalRouter.Run() ─── 每秒轮询
|
||
│
|
||
▼
|
||
检测信号变化(上升沿/下降沿)
|
||
│
|
||
▼
|
||
根据信号找到对应 Actor
|
||
│
|
||
▼
|
||
Actor.HandleSignal() ─── 更新槽位状态,发布事件
|
||
│
|
||
▼
|
||
EventLoop 收到事件 ─── 触发调度器
|
||
```
|
||
|
||
### 6.2 信号类型
|
||
|
||
**设备完成信号(PLC → 系统):**
|
||
每种设备类型可定义自己的完成信号,由 `done_signal_name` 字段指定。信号按设备类型分组:
|
||
|
||
| 设备类型 | 完成信号 | 处理方 | 触发效果 |
|
||
|----------|----------|--------|---------|
|
||
| GRINDER | GrinderMachiningDone | MachineActor | 槽位状态 PROCESSING → DONE |
|
||
| TURN_TABLE | TurnTableExchangeDone | MachineActor | 槽位状态 PROCESSING → DONE |
|
||
| AIR_BLOW | WashCompleteDone | MachineActor | 槽位状态 PROCESSING → DONE |
|
||
| FULL_CHECK | FullCheckOK / FullCheckNG | MachineActor | 检测结果分支(OK→继续 / NG→报废) |
|
||
|
||
**请求信号(系统 → PLC):**
|
||
系统通过 `PlcExecutor` 向 PLC 发送请求信号,信号命名规范为 `{设备}{动作}Req`,等待对应的 `{设备}{动作}Done` 完成信号。
|
||
|
||
### 6.3 信号 vs 状态
|
||
|
||
**重要原则:PLC 信号是输入,不是状态源。**
|
||
|
||
- 系统以数据库中的状态为准
|
||
- PLC 信号只用来触发状态迁移
|
||
- 信号可能丢失或重复,系统通过幂等设计保证正确性
|
||
|
||
---
|
||
|
||
## 7. 事件系统
|
||
|
||
### 7.1 事件总线
|
||
|
||
当前使用 `LocalBus`(内存实现),基于 Go channel 的发布/订阅模式。
|
||
|
||
**事件结构:**
|
||
```go
|
||
type Event struct {
|
||
ID string // UUID
|
||
Type string // 事件类型
|
||
Source string // 来源
|
||
Timestamp time.Time
|
||
Payload map[string]interface{}
|
||
}
|
||
```
|
||
|
||
### 7.2 事件类型
|
||
|
||
| 事件类型 | 发布者 | 订阅者 | 作用 |
|
||
|----------|--------|--------|------|
|
||
| 设备信号事件 | SignalRouter | EventLoop | 驱动状态机 |
|
||
| 工单创建事件 | 前端 API | JobProcessor | 开始生产 |
|
||
| 槽位状态变更 | Actor | EventLoop | 触发调度 |
|
||
| 调度任务下发 | EventLoop | PlcWorker | 执行动作 |
|
||
| 告警事件 | 各组件 | AlarmService | 通知 |
|
||
| 人工确认事件 | RecoveryCoordinator | 前端 | 断电恢复 |
|
||
|
||
### 7.3 事件订阅者
|
||
|
||
当前所有订阅者:
|
||
- **EventLoop** — 订阅 PLC 信号和调度结果
|
||
- **AlarmService** — 订阅告警事件
|
||
- **EventLogWriter** — 订阅所有事件,写入数据库审计
|
||
- **SSEBridge** — 订阅所有事件,推送到前端看板
|
||
|
||
### 7.4 启动顺序保证
|
||
|
||
事件总线先创建、后启动:
|
||
1. 创建总线(`NewLocalBus`)— 此时可以订阅,但不分发事件
|
||
2. 各组件注册订阅
|
||
3. 启动总线(`bus.Start`)— 开始分发事件
|
||
|
||
防止"事件发了但没人处理"的竞态条件。
|
||
|
||
---
|
||
|
||
## 8. 调度器
|
||
|
||
### 8.1 调度触发
|
||
|
||
事件循环收到"槽位状态变化"事件后,触发调度器:
|
||
- 工件变为 `ON_BUFFER`(暂存台上就绪)
|
||
- 工件变为 `WAITING_UNLOAD`(设备加工完成待取走)
|
||
|
||
### 8.2 决策过程
|
||
|
||
```
|
||
输入:就绪工件列表
|
||
│
|
||
▼
|
||
任务生成层 ─── 根据工件当前步骤,生成候选动作
|
||
│
|
||
▼
|
||
约束过滤层 ─── 排除不可用的设备和冲突
|
||
│
|
||
▼
|
||
策略选择层 ─── 按优先级排序,选择最优方案
|
||
│
|
||
▼
|
||
输出:搬运任务(从哪取,放到哪)
|
||
```
|
||
|
||
### 8.3 优先级策略
|
||
|
||
1. **紧急转移** — WAITING_UNLOAD 的工件必须尽快取走,否则堵住设备
|
||
2. **成组装卸** — 清洗机需要凑够 2 件才关门
|
||
3. **设备上料** — 给空闲设备上料
|
||
4. **批量补料** — 暂存台空位 > 4 时触发
|
||
5. **手持作业** — 扫码、打标
|
||
6. **普通搬运** — 常规的暂存台 ↔ 设备搬运
|
||
|
||
---
|
||
|
||
## 9. 数据流与写入路径
|
||
|
||
### 9.1 写入路径
|
||
|
||
```
|
||
事件源(PLC 信号 / 前端指令 / 调度结果)
|
||
│
|
||
▼
|
||
EventLoop 处理事件
|
||
│
|
||
├──→ 更新 Actor 内存状态
|
||
│
|
||
├──→ 触发调度器
|
||
│
|
||
└──→ 写入数据库(唯一写 DB 入口)
|
||
```
|
||
|
||
**关键约束:** EventLoop 是唯一写数据库的入口。Actor 不能直接操作数据库。
|
||
|
||
### 9.2 数据一致性
|
||
|
||
```
|
||
EventLoop 处理事件
|
||
│
|
||
├──1. 更新 PostgreSQL(持久化)
|
||
│
|
||
└──2. 发布事件到总线(通知其他组件)
|
||
```
|
||
|
||
先写数据库,后发事件。如果写库失败,事件不发布。
|
||
|
||
### 9.3 事件日志
|
||
|
||
所有总线事件通过 `EventLogWriter` 异步写入 `event_log` 表:
|
||
- 用于审计追溯
|
||
- 用于断电恢复重放
|
||
|
||
---
|
||
|
||
## 10. 断电恢复
|
||
|
||
### 10.1 恢复分级
|
||
|
||
| 级别 | 条件 | 处理方式 |
|
||
|------|------|---------|
|
||
| **L1 自动** | 工件在接驳台(CREATED)、暂存台(ON_BUFFER)、等待资源 | 直接恢复,无需人工 |
|
||
| **L2 半自动** | 设备加工中(PROCESSING),PLC 确认完成 | 转为 WAITING_UNLOAD,自动恢复 |
|
||
| **L3 人工确认** | 机器人操作中(IN_HANDLING)、关门中、位置不明 | 发布人工确认事件,前端通知操作员 |
|
||
|
||
### 10.2 恢复流程
|
||
|
||
```
|
||
1. 从数据库读取所有未完成的 Job 和 Task
|
||
2. 按分级分类
|
||
3. L1 → 直接恢复状态
|
||
4. L2 → 查询 PLC 确认设备状态,恢复
|
||
5. L3 → 发布 ManualActionCreatedEvent 到总线
|
||
6. 前端收到 → 弹出人工确认对话框
|
||
7. 操作员确认后 → 恢复 Job
|
||
```
|
||
|
||
### 10.3 事件重放
|
||
|
||
从 `event_log` 表按顺序重放事件,版本号和幂等性保证正确性:
|
||
- 版本号校验:事件版本必须等于当前实体版本
|
||
- 幂等:重复事件(相同 `last_event_id`)直接跳过
|
||
|
||
---
|
||
|
||
## 11. 数据库设计
|
||
|
||
### 11.1 主数据表
|
||
|
||
| 表名 | 用途 | 备注 |
|
||
|------|------|------|
|
||
| `product_type` | 产品类型定义 | 关联 Recipe |
|
||
| `recipe` | 工艺配方 | 静态配置 |
|
||
| `recipe_step` | 配方步骤 | 步骤类型、资源类型、默认下一步 |
|
||
| `equipment` | 设备定义 | 来自 PLC 配置 |
|
||
| `equipment_slot` | 设备槽位(运行时) | 唯一槽位真相源,包含 `dock_no` 区分左右托盘 |
|
||
|
||
### 11.2 业务数据表
|
||
|
||
| 表名 | 用途 | 备注 |
|
||
|------|------|------|
|
||
| `work_order` | 工单 | 关联产品类型、数量、状态 |
|
||
| `job` | 工件实例 | 当前步骤、状态、位置、版本 |
|
||
| `job_step_instance` | 工序执行记录 | 每次工序的执行记录 |
|
||
| `task` | 搬运任务 | 机器人执行的任务记录 |
|
||
| `event_log` | 事件日志 | 审计和恢复用 |
|
||
|
||
### 11.3 关键设计
|
||
|
||
- `equipment_slot` 是槽位状态的**唯一真相源**,包含 `status`、`current_job_id`、`occupied_at` 等字段
|
||
- `job` 表使用 `version` 字段实现乐观锁,防止并发冲突
|
||
- `job` 表使用 `last_event_id` 字段实现幂等
|
||
|
||
---
|
||
|
||
## 12. PLC 信号映射
|
||
|
||
### 12.1 信号模型
|
||
|
||
PLC 信号分为两类,全部定义在 `doc/signals.sql` 中:
|
||
|
||
**状态信号(PLC → 系统):**
|
||
PLC 向系统上报设备状态变化,由 `SignalRouter` 轮询检测上升沿后投递给 EventLoop。
|
||
|
||
**指令信号(系统 → PLC):**
|
||
系统向 PLC 下发动作指令,由 `PlcExecutor` 按握手协议执行(写 Req → 等 Done → 复位 Req)。
|
||
|
||
具体信号地址和映射关系见 `doc/signals.sql`,不同项目替换此文件即可。
|
||
|
||
### 12.2 信号握手协议
|
||
|
||
所有 PLC 动作遵循统一的四步握手协议:
|
||
|
||
```
|
||
1. 写参数字节(如位置号、托盘号),非必须
|
||
2. 置位 Req 信号 = true,触发 PLC 执行动作
|
||
3. 轮询等待 Done 信号 = true(上升沿检测,间隔 100ms,超时 30s)
|
||
4. 复位 Req 信号 = false
|
||
5. 等待 Done 信号 = false(PLC 看到 Req 为 false 后复位 Done,握手完成)
|
||
```
|
||
|
||
信号命名规范:`{设备名}{动作}Req` / `{设备名}{动作}Done` 成对出现。
|
||
|
||
---
|
||
|
||
## 13. 关键技术选型
|
||
|
||
| 组件 | 技术 | 选型理由 |
|
||
|------|------|---------|
|
||
| 后端语言 | Go 1.23.4 | 并发性能好,编译快,单二进制部署 |
|
||
| 框架 | go-zero v1.3.8 | 轻量级,支持 HTTP/API 代码生成 |
|
||
| ORM | ent v0.14.5 | 类型安全,代码生成 |
|
||
| 数据库 | PostgreSQL | 稳定可靠,支持 JSONB |
|
||
| Token 存储 | 内存(InMemoryTokenStore) | 单进程部署,无需外部依赖 |
|
||
| PLC 通信 | Snap7(西门子 S7 协议) | 工业标准协议 |
|
||
| 前端 | React 19 + Semi Design | 企业级 UI 组件库
|
||
|
||
---
|
||
|
||
## 14. 已知问题与待办
|
||
|
||
### 14.1 GrinderMachiningDone 信号初始化
|
||
|
||
`doc/signals.sql` 中已定义该信号,但需执行 `go run data.go signals` 导入到数据库。
|
||
|
||
### 14.2 RecipeLoader 调用方
|
||
|
||
`RecipeLoader` 提供按产品类型加载工艺路线的缓存能力,目前仅用于 `cmd/mockrun` 调试工具。核心业务路径(EventLoop / DBState)直接通过 `entClient.RecipeStep.Query()` 查库。
|
||
|
||
---
|
||
|
||
## 15. 安全防护架构
|
||
|
||
### 15.1 宕机风险防护
|
||
|
||
事件循环是产线的核心状态机,一旦崩溃将导致生产中断。系统采用多层次防护机制:
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────┐
|
||
│ 应用层防护 │
|
||
│ ├── panic recover(EventLoop.Run + goroutine) │
|
||
│ ├── workerBusy 超时保护(5分钟自动重置) │
|
||
│ └── 通道背压保护(非阻塞发送 + 告警) │
|
||
├──────────────────────────────────────────────────────┤
|
||
│ 业务层防护 │
|
||
│ ├── WorkerResult 幂等性(taskID 去重) │
|
||
│ ├── Exchange LoadJob 失败同步处理 │
|
||
│ ├── 槽位分配 fail-fast(不 fallback 到槽位1) │
|
||
│ └── 全检台 RequestInspect 去重 │
|
||
├──────────────────────────────────────────────────────┤
|
||
│ 数据层防护 │
|
||
│ ├── 数据库查询超时(context.WithTimeout) │
|
||
│ ├── DB 写操作 error 必须处理 │
|
||
│ ├── Exchange 原子化操作 │
|
||
│ └── 断电恢复数据一致性校验 │
|
||
└──────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 15.2 并发安全设计
|
||
|
||
| 场景 | 风险 | 防护措施 |
|
||
|------|------|---------|
|
||
| goroutine 访问共享 map | `concurrent map read/write` | 启动前提取局部变量传入 |
|
||
| 类型断言失败 | panic | 带 ok 检查的类型断言 |
|
||
| JSON 反序列化类型不匹配 | panic | `intFromPayload` 兼容 int/int64/float64 |
|
||
| map key 不存在 | 零值静默使用 | `v, ok := m[key]` 模式 |
|
||
|
||
### 15.3 待修复的安全问题
|
||
|
||
| 优先级 | 问题 | 位置 | 修复方案 |
|
||
|--------|------|------|---------|
|
||
| P0 | EventLoop.Run() 缺少 panic recover | `loop.go:274-300` | 添加 defer recover |
|
||
| P0 | dispatchWorker goroutine 缺少 recover | `worker_dispatch.go:223` | 添加 defer recover |
|
||
| P0 | workerBusy 缺少超时保护 | `loop.go:64` | 添加 workerStartedAt + 超时检查 |
|
||
| P0 | snap 空指针访问 | `worker_dispatch.go:79` | 添加 nil 检查 |
|
||
| P1 | DB 查询缺少超时 | `scheduler_bridge.go:47` | 添加 context.WithTimeout |
|
||
| P1 | handleWorkerResult 缺少幂等性 | `worker_dispatch.go:189-220` | 添加 taskID 去重 |
|
||
| P1 | Exchange LoadJob 失败未处理 | `worker_dispatch.go:428-462` | 检查 LoadJobID > 0 |
|
||
| P1 | 槽位分配失败 fallback 到槽位1 | `worker_dispatch.go:319-321` | 改为 fail-fast |
|
||
| P2 | 全检台 RequestInspect 重复触发 | `worker_dispatch.go:471-507` | 添加原子标志去重 |
|
||
| P2 | 全检台容量硬编码为6 | `worker_dispatch.go:483` | 从 Actor SlotCount 获取 |
|
||
| P2 | msgCh 通道满时阻塞 | `loop.go:199` | 改为 select + default |
|
||
|
||
---
|
||
|
||
## 16. 代码结构优化建议
|
||
|
||
### 16.1 函数拆分
|
||
|
||
| 函数 | 当前行数 | 问题 | 拆分建议 |
|
||
|------|----------|------|---------|
|
||
| `dispatchWorker` | ~130 行 | 超过 80 行约束 | 拆分为 fillActionSlots、executeAndSyncActor、buildWorkerResultMessage |
|
||
| `trySchedule` | ~200 行 | 承担 5 个职责 | 拆分为 cleanupExpiredSnapshots、refreshRuntimeSnapshots、buildJobViews、buildSystemState、dispatchCandidates |
|
||
| `buildSystemState` | ~150 行 | 承担 6 个子逻辑 | 拆分为 buildMachineBusyState、buildMachineSlotState、buildCacheSlotState、buildDockSlotState、buildOrderState、buildExchangePairState |
|
||
| `handleMachineDone` | ~70 行 | 嵌套过深 | 拆分为 handleInspectionResult、recordCNCTime、advanceJobStep |
|
||
|
||
### 16.2 代码质量改进
|
||
|
||
| 问题 | 位置 | 改进方案 |
|
||
|------|------|---------|
|
||
| Actor 查找重复 5 次 | `worker_dispatch.go:102-144` | 分支前一次取出 Actor |
|
||
| handleWorkerSuccess 分支条件风格不一致 | `worker_dispatch.go:242-264` | 统一为 switch payload.Action |
|
||
| tryRequestFullCheckInspect 嵌套过深 | `worker_dispatch.go:486-497` | 拆为 isFullCheckFull、isLastPieceOfOrder |
|
||
| handleExchangeLoadJob 混合两条路径 | `worker_dispatch.go:389-426` | 拆为 handleExchangeLoadJobForOperation、handleExchangeLoadJobForAdvance |
|
||
| tryRequestFullCheckInspect 文件归属不当 | `worker_dispatch.go:471-507` | 移到 candidate_handlers.go 或新建 fullcheck_handler.go |
|
||
| success 判断逻辑分散 | `worker_dispatch.go:203` | 在 types.go 增加 func (t MessageType) IsSuccess() bool |
|
||
| UpdateOneID 影响行数被忽略 | 多处 | 检查 n == 0 时 slog.Warn |
|
||
|
||
---
|
||
|
||
## 17. BuildProductionLine 详细流程
|
||
|
||
`BuildProductionLine` 是产线初始化的核心入口,位于 `internal/processor/factory.go`。
|
||
|
||
### 执行步骤
|
||
|
||
```
|
||
1. buildSignalMaps(ctx, entClient)
|
||
├── 查询 equipment 表,读取 done_signal_name 字段
|
||
├── 调用 preload.GetMAddress(signalName) 查找 PLC 地址
|
||
├── 成功 → 写入 doneSignals[equipmentID] = PLC地址
|
||
└── 失败 → slog.Warn
|
||
|
||
2. BuildActors(ctx, entClient)
|
||
├── 查询 equipment 表,遍历每台设备
|
||
├── NewMachineActor(cfg) → 创建带锁状态结构体
|
||
└── machineActors[eq.ID] = actor
|
||
|
||
3. NewSignalRouter(plc) → 创建信号路由器
|
||
├── 遍历 equipment 表,对每台设备注册完成信号
|
||
└── signalRouter.Watch(eq.ID, doneSignals[eq.ID])
|
||
|
||
4. 返回 ProductionLine{Actors, SignalRouter}
|
||
```
|
||
|
||
### 关键设计决策
|
||
|
||
| 决策 | 说明 |
|
||
|------|------|
|
||
| Actor 不启动 goroutine | Actor 是纯状态结构体,由 EventLoop 同步调用 |
|
||
| 设备 PLC 握手由 PlcExecutor 统一管理 | 不同设备类型的 PLC 操作通过 PlcExecutor 方法实现,PlcWorker 直接调用,无中间 Station 抽象 |
|
||
| SignalRouter 在 Build 时不绑定 sink | EventLoop 尚未创建,由 `service_context.go` 后续绑定 |
|
||
| 不在此处写 DB | 只读 equipment 表,所有写入由 EventLoop 统一完成 |
|
||
|
||
---
|
||
|
||
*本方案与代码保持同步,修改代码前请先更新此文档。* |