Files
bj_power/bj_power_mes/docs/superpowers/plans/2026-05-12-washer-batch-load-unload-plan.md
T

587 lines
18 KiB
Markdown
Raw Normal View History

# 高压清洗机批量上料/下料/换料 实施计划
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 高压清洗机支持灵活批量上料(有2个上2个,有1个上1个)、逐槽位判断换料/卸载、RobotAction 数组串行执行。
**Architecture:**`trySchedule` dispatch 阶段查询 WASHER_HP 槽位状态,编排 `[]RobotAction` 数组一次性 dispatch。Worker 串行执行数组中每个 action。StartupWasher 作为独立 RobotAction,由编排决定何时追加。
**Tech Stack:** Go 1.23, ent ORM, PostgreSQL
---
## Chunk 1: HardwareWorker 接口 + RobotWorker 适配
### Task 1: HardwareWorker 接口改为接收 []RobotAction
**Files:**
- Modify: `internal/action/action.go:33-35`
- Test: `internal/eventloop/loop_test.go`
- [ ] **Step 1: 修改 HardwareWorker 接口**
`action.go``HardwareWorker` 接口从单个 action 改为 action 数组:
```go
// HardwareWorker 硬件执行器接口
type HardwareWorker interface {
Execute(ctx context.Context, actions []RobotAction) error
}
```
- [ ] **Step 2: 适配 RobotWorker.Execute**
修改 `internal/processor/robot_worker.go``Execute` 方法,串行执行 action 数组:
```go
func (w *RobotWorker) Execute(ctx context.Context, actions []action.RobotAction) error {
for _, act := range actions {
slog.Info("robot worker: executing", "kind", act.Kind, "jobId", act.JobID)
if err := w.executeSingle(ctx, act); err != nil {
return fmt.Errorf("action %s failed: %w", act.Kind, err)
}
}
return nil
}
// executeSingle 执行单个 RobotAction
func (w *RobotWorker) executeSingle(ctx context.Context, act action.RobotAction) error {
switch act.Kind {
case "scan_full":
return w.executeScanFull(ctx, act)
case "mark_full":
return w.executeMarkFull(ctx, act)
case "load_to_machine":
return w.executeLoadToMachine(ctx, act)
case "unload_to_buffer":
return w.executeUnloadToBuffer(ctx, act)
case "exchange_full":
return w.executeExchangeFull(ctx, act)
case "unload_to_dock":
return w.executeUnloadToDock(ctx, act)
case "batch_unload":
return w.executeBatchUnload(ctx, act)
case "startup_washer":
return w.executeStartupWasher(ctx, act)
default:
return fmt.Errorf("unknown action kind: %s", act.Kind)
}
}
```
- [ ] **Step 3: 新增 executeStartupWasher 方法**
`internal/processor/robot_worker.go` 中新增:
```go
// executeStartupWasher 启动高压清洗机
func (w *RobotWorker) executeStartupWasher(ctx context.Context, act action.RobotAction) error {
if w.robotCtrl == nil {
return fmt.Errorf("startup_washer: robot controller not available")
}
if err := w.robotCtrl.StartupWasher(ctx); err != nil {
return fmt.Errorf("startup_washer: %w", err)
}
return nil
}
```
- [ ] **Step 4: 移除 executeLoadToMachine 中 machineID==5 的 StartupWasher 调用**
删除 `internal/processor/robot_worker.go:184-189` 的代码块:
```go
// 删除这段:
if machineID == 5 {
if err := w.robotCtrl.StartupWasher(ctx); err != nil {
return fmt.Errorf("load: startup washer: %w", err)
}
}
```
- [ ] **Step 5: 适配 eventloop/worker.go 类型别名**
`internal/eventloop/worker.go` 中的类型别名无需修改(`RobotAction``HardwareWorker` 仍是 `action` 包的类型别名,接口变更自动传播)。
- [ ] **Step 6: 适配 dispatchWorker 调用**
修改 `internal/eventloop/loop.go``dispatchWorker` 方法,将 `l.worker.Execute(ctx, act)` 改为 `l.worker.Execute(ctx, []action.RobotAction{act})`。这是临时适配,后续 Task 会改为传递 action 数组。
- [ ] **Step 7: 适配 replenishWorker 和其他调用点**
搜索所有 `l.worker.Execute` 调用点,统一改为传递 `[]RobotAction` 切片。当前只有 `dispatchWorker` 一处调用。
- [ ] **Step 8: 编译验证**
Run: `rtk go build ./internal/action/ ./internal/processor/ ./internal/eventloop/`
- [ ] **Step 9: 运行现有测试**
Run: `rtk go test ./internal/eventloop/... ./internal/processor/... -count=1`
- [ ] **Step 10: Commit**
```bash
rtk git add internal/action/action.go internal/processor/robot_worker.go internal/eventloop/loop.go
rtk git commit -m "feat(worker): HardwareWorker 接收 []RobotAction,新增 startup_washer action"
```
---
## Chunk 2: WASHER_HP 上料编排
### Task 2: trySchedule 中 WASHER_HP Load 批量编排
**Files:**
- Modify: `internal/eventloop/loop.go:347-381` (trySchedule dispatch 阶段)
- Test: `internal/eventloop/loop_test.go`
- [ ] **Step 1: 新增 findSecondWasherLoadCandidate 辅助方法**
`internal/eventloop/loop.go` 中新增方法,查找是否有第二个待清洗工件可以同时上料:
```go
// findSecondWasherLoadCandidate 查找第二个等待 WASHER_HP 上料的候选任务
func (l *ProductionEventLoop) findSecondWasherLoadCandidate(candidates []scheduler.CandidateTask, skipJobID int) *scheduler.CandidateTask {
for i := range candidates {
ct := &candidates[i]
if ct.JobID == skipJobID {
continue
}
if ct.Action == scheduler.ActionLoadWasher {
snap := l.jobRuntimes[ct.JobID]
if snap == nil {
continue
}
// 分配目标设备
if ct.TargetID == 0 {
ct.TargetID = l.findIdleMachine(snap.ResourceType, snap.ProductTypeID)
}
if ct.TargetID > 0 {
return ct
}
}
}
return nil
}
```
- [ ] **Step 2: 新增 buildWasherLoadActions 方法**
构建 WASHER_HP 上料的 `[]RobotAction` 数组:
```go
// buildWasherLoadActions 构建 WASHER_HP 上料 action 数组
// 1个候选 → Load + StartupWasher
// 2个候选 → Load1 + Load2 + StartupWasher
func (l *ProductionEventLoop) buildWasherLoadActions(ctx context.Context, ct scheduler.CandidateTask, snap *RuntimeSnapshot, secondCT *scheduler.CandidateTask) []action.RobotAction {
var actions []action.RobotAction
// 第一个 Load
actions = append(actions, l.candidateToRobotAction(ctx, ct, snap))
// 第二个 Load(如果有)
if secondCT != nil {
secondSnap := l.jobRuntimes[secondCT.JobID]
if secondSnap != nil {
actions = append(actions, l.candidateToRobotAction(ctx, *secondCT, secondSnap))
}
}
// StartupWasher(有 Load 就启动)
actions = append(actions, action.RobotAction{
Kind: "startup_washer",
JobID: ct.JobID,
WorkpieceType: snap.ProductTypeID,
})
return actions
}
```
- [ ] **Step 3: 修改 trySchedule dispatch 阶段**
`trySchedule` 的候选任务循环中,当遇到 `ActionLoadWasher` 时,走批量编排路径:
```go
// 在 ct.Action.IsLoad() 分支内,WASHER_HP 特殊处理
if ct.Action == scheduler.ActionLoadWasher {
// 查找是否有第二个候选
secondCT := l.findSecondWasherLoadCandidate(candidates, ct.JobID)
actions := l.buildWasherLoadActions(ctx, ct, snap, secondCT)
l.workerBusy.Store(true)
go l.dispatchWorkerActions(ctx, ct.JobID, actions)
// 标记第二个候选已消费
if secondCT != nil {
consumed[secondCT.JobID] = true
}
continue
}
```
- [ ] **Step 4: 新增 dispatchWorkerActions 方法**
替代原 `dispatchWorker`,接收 `[]RobotAction` 数组:
```go
// dispatchWorkerActions 异步执行 Worker 动作数组,完成后回投 WorkerResult
func (l *ProductionEventLoop) dispatchWorkerActions(ctx context.Context, primaryJobID int, actions []action.RobotAction) {
taskID := fmt.Sprintf("batch-%d-%d", primaryJobID, time.Now().UnixNano())
kinds := make([]string, len(actions))
for i, a := range actions {
kinds[i] = a.Kind
}
slog.Info("event loop: dispatch actions", "taskID", taskID, "actions", kinds)
err := l.worker.Execute(ctx, actions)
msgType := ResRobotActionSucceeded
if err != nil {
slog.Error("event loop: worker actions failed", "jobId", primaryJobID, "error", err)
msgType = ResRobotActionFailed
}
payload := map[string]any{
"jobId": primaryJobID,
"actions": kinds,
"actionCount": len(actions),
}
// 收集每个 action 的 JobID 和目标设备信息
var jobIDs []int
var targetIDs []int
for _, a := range actions {
jobIDs = append(jobIDs, a.JobID)
targetIDs = append(targetIDs, a.DstMachine)
}
payload["jobIds"] = jobIDs
payload["targetIDs"] = targetIDs
l.Send(EventLoopMessage{
ID: taskID,
Type: msgType,
CorrelationID: taskID,
Payload: payload,
CreatedAt: time.Now(),
})
}
```
- [ ] **Step 5: 编译验证**
Run: `rtk go build ./internal/eventloop/`
- [ ] **Step 6: Commit**
```bash
rtk git add internal/eventloop/loop.go
rtk git commit -m "feat(eventloop): WASHER_HP 上料批量编排,Load+StartupWasher 或 Load1+Load2+StartupWasher"
```
---
## Chunk 3: WASHER_HP 卸载/换料编排
### Task 3: trySchedule 中 WASHER_HP Unload/Exchange 批量编排
**Files:**
- Modify: `internal/eventloop/loop.go` (trySchedule dispatch 阶段 + candidateToRobotAction)
- Test: `internal/eventloop/loop_test.go`
- [ ] **Step 1: 新增 buildWasherUnloadActions 方法**
构建 WASHER_HP 卸载/换料的 `[]RobotAction` 数组。当 dispatch 一个 Unload/Exchange 时,检查 `batchMachines` 决定是否追加第二个 action
```go
// buildWasherUnloadActions 构建 WASHER_HP 卸载/换料 action 数组
// 检查 batch 设备另一个槽位状态,按需追加 action
func (l *ProductionEventLoop) buildWasherUnloadActions(ctx context.Context, ct scheduler.CandidateTask, snap *RuntimeSnapshot) []action.RobotAction {
var actions []action.RobotAction
// 第一个 actionUnload 或 Exchange
actions = append(actions, l.candidateToRobotAction(ctx, ct, snap))
machineID, _ := parsePositionRef(snap.PositionRefID)
if machineID <= 0 || !l.batchMachines[machineID] {
return actions
}
// 查询同设备另一个槽位状态
doneSlots := l.db.FindAllDoneJobsOnMachine(ctx, machineID)
emptySlots := 0
if l.registry != nil {
if st, ok := l.registry.Get(strconv.Itoa(machineID)); ok {
if counter, ok := st.(interface{ EmptySlotCount() int }); ok {
emptySlots = counter.EmptySlotCount()
}
}
}
// 另一个槽位有 Done 工件
if len(doneSlots) > 1 {
// 找到第二个 Done 槽位(不是当前 Job 的)
for _, ds := range doneSlots {
if ds.JobID == ct.JobID {
continue
}
dsnap := l.jobRuntimes[ds.JobID]
if dsnap == nil {
continue
}
// 检查是否有配对
loadJobID := l.findExchangePair(dsnap)
if loadJobID > 0 {
// 追加 Exchange
loadSnap := l.jobRuntimes[loadJobID]
loadTempSlot := 0
if loadSnap != nil {
loadTempSlot = loadSnap.TempSlotNo
}
actions = append(actions, action.RobotAction{
Kind: "exchange_full",
JobID: ds.JobID,
WorkpieceType: dsnap.ProductTypeID,
SrcSlot: loadTempSlot,
DstSlot: dsnap.TempSlotNo,
DstMachine: machineID,
Extra: map[string]any{"loadJobID": loadJobID},
})
} else {
// 追加 Unload
actions = append(actions, action.RobotAction{
Kind: "unload_to_buffer",
JobID: ds.JobID,
WorkpieceType: dsnap.ProductTypeID,
SrcMachine: machineID,
DstSlot: dsnap.TempSlotNo,
})
}
break // 只处理一个额外槽位
}
} else if emptySlots > 0 {
// 另一个槽位空,检查是否有待上料工件
loadJobID := l.findExchangePair(snap)
if loadJobID > 0 {
loadSnap := l.jobRuntimes[loadJobID]
if loadSnap != nil {
actions = append(actions, action.RobotAction{
Kind: "load_to_machine",
JobID: loadJobID,
WorkpieceType: loadSnap.ProductTypeID,
SrcSlot: loadSnap.TempSlotNo,
DstMachine: machineID,
})
}
}
}
// 启动判断:actions 中包含 Exchange 或 Load → 追加 StartupWasher
hasLoadOrExchange := false
for _, a := range actions {
if a.Kind == "exchange_full" || a.Kind == "load_to_machine" {
hasLoadOrExchange = true
break
}
}
if hasLoadOrExchange {
actions = append(actions, action.RobotAction{
Kind: "startup_washer",
JobID: ct.JobID,
WorkpieceType: snap.ProductTypeID,
})
}
return actions
}
```
- [ ] **Step 2: 修改 trySchedule dispatch 阶段**
`trySchedule` 的候选任务循环中,当遇到 WASHER_HP Unload/Exchange 时,走批量编排路径:
```go
// WASHER_HP Unload/Exchange 特殊处理
if (ct.Action == scheduler.ActionUnloadWasher || ct.Action == scheduler.ActionExchange) && snap.ResourceType == "WASHER_HP" {
actions := l.buildWasherUnloadActions(ctx, ct, snap)
l.workerBusy.Store(true)
go l.dispatchWorkerActions(ctx, ct.JobID, actions)
continue
}
```
- [ ] **Step 3: 移除 candidateToRobotAction 中 WASHER_HP batch_unload 逻辑**
删除 `candidateToRobotAction``snap.ResourceType == "WASHER_HP" && mid > 0``batch_unload` 分支(`loop.go:685-704`),改为直接返回 `unload_to_buffer`
```go
// 替换原来的 WASHER_HP batch_unload 逻辑
if ct.Action.IsUnload() {
mid, _ := parsePositionRef(snap.PositionRefID)
return action.RobotAction{
Kind: "unload_to_buffer",
JobID: ct.JobID,
WorkpieceType: wt,
SrcMachine: mid,
DstSlot: ts,
}
}
```
- [ ] **Step 4: 编译验证**
Run: `rtk go build ./internal/eventloop/`
- [ ] **Step 5: Commit**
```bash
rtk git add internal/eventloop/loop.go
rtk git commit -m "feat(eventloop): WASHER_HP 卸载/换料批量编排,逐槽位判断 Exchange/Unload"
```
---
## Chunk 4: handleWorkerResult 适配
### Task 4: handleWorkerResult 处理多 action 结果
**Files:**
- Modify: `internal/eventloop/loop.go:1062-1253` (handleWorkerResult)
- Test: `internal/eventloop/loop_test.go`
- [ ] **Step 1: 修改 handleWorkerResult 识别多 action 结果**
`dispatchWorkerActions` 返回的 payload 包含 `actionCount > 1` 时,按顺序处理每个 action 的状态变更。核心逻辑:
- 从 payload 中提取 `jobIds``targetIDs` 数组
- 对每个 action,根据 kind 执行对应的状态推进逻辑(复用现有 handleWorkerResult 中的单 action 逻辑)
- `startup_washer` action 无状态变更
- 任一 action 失败 → 整个批次失败,已成功的 action 状态保留
- [ ] **Step 2: 新增 handleSingleActionResult 辅助方法**
将现有 handleWorkerResult 中单 action 的状态推进逻辑提取为独立方法:
```go
// handleSingleActionResult 处理单个 action 成功后的状态推进
func (l *ProductionEventLoop) handleSingleActionResult(ctx context.Context, jobID int, actKind string, targetID int, assignedSlot int) {
// 根据 actKind 执行对应的状态推进:
// load_to_machine → AdvanceStep + SetStatus(Processing)
// unload_to_buffer → AdvanceStep + SetStatus(OnBuffer)
// exchange_full → AdvanceStep(doneJob) + AdvanceStep(loadJob) + SetStatus(Processing)
// startup_washer → 无操作
// unload_to_dock → AdvanceStep + ClearTempSlotNo
// scan_full / mark_full → 更新 context
}
```
- [ ] **Step 3: 编译验证**
Run: `rtk go build ./internal/eventloop/`
- [ ] **Step 4: 运行测试**
Run: `rtk go test ./internal/eventloop/... -count=1`
- [ ] **Step 5: Commit**
```bash
rtk git add internal/eventloop/loop.go
rtk git commit -m "feat(eventloop): handleWorkerResult 适配多 action 结果处理"
```
---
## Chunk 5: MockPLC 适配 + 集成测试
### Task 5: MockPLC StartupWasher 信号适配
**Files:**
- Modify: `internal/mock/plc.go`
- Test: `internal/eventloop/loop_test.go`
- [ ] **Step 1: 确认 MockPLC StartupWasher 信号路径**
当前 MockPLC 中 `triggerMachineDone``case 5` 使用 `washerDone` atomic 防重复触发。新逻辑下 `StartupWasher` 作为独立 action 调用 `robotCtrl.StartupWasher(ctx)`,该方法写入 `WasherStartup` PLC 地址。
检查 MockPLC 是否有 `WasherStartup` 地址的 auto-reply 规则。如果没有,需要添加:当 `WasherStartup` 信号被写入时,触发 4s 计时后设置 `WasherDone` 信号。
- [ ] **Step 2: 添加 WasherStartup auto-reply 规则(如需要)**
`initAutoReplyRules` 中添加:
```go
// WasherStartup (M1xxx): 启动清洗机,4s 后触发 WasherDone
// 需要确认 WasherStartup 对应的 PLC 地址
```
注意:如果 `StartupWasher` 已经通过 `PlaceWorkpieceToMachine``case 5` 路径触发,则不需要额外规则。需要确认 `StartupWasher` 写入的地址是否与 `PlaceWorkpieceToMachine``case 5` 路径一致。
- [ ] **Step 3: 编译验证**
Run: `rtk go build ./internal/mock/`
- [ ] **Step 4: Commit**
```bash
rtk git add internal/mock/plc.go
rtk git commit -m "feat(mock): StartupWasher 信号适配"
```
### Task 6: 端到端集成测试
**Files:**
- Test: `internal/eventloop/loop_test.go`
- [ ] **Step 1: 编写 WASHER_HP 批量上料测试**
测试场景:
- 1个待清洗工件 → 生成 `[]RobotAction{Load, StartupWasher}`
- 2个待清洗工件 → 生成 `[]RobotAction{Load1, Load2, StartupWasher}`
- [ ] **Step 2: 编写 WASHER_HP 卸载/换料测试**
测试场景:
- 2个 Done0个配对 → `Unload + Unload`
- 2个 Done1个配对 → `Exchange + Unload + StartupWasher`
- 2个 Done2个配对 → `Exchange + Exchange + StartupWasher`
- 1个 Done0个配对 → `Unload`
- 1个 Done1个配对 → `Exchange + Load + StartupWasher`
- [ ] **Step 3: 运行全部测试**
Run: `rtk go test ./internal/eventloop/... ./internal/processor/... -count=1`
- [ ] **Step 4: Commit**
```bash
rtk git add internal/eventloop/loop_test.go
rtk git commit -m "test(eventloop): WASHER_HP 批量上料/卸载/换料集成测试"
```
---
## Chunk 6: Mock 模式端到端验证
### Task 7: Mock 模式完整流程验证
**Files:**
- No new files (验证现有 mockrun)
- [ ] **Step 1: 运行 mockrun 验证完整流程**
Run: `rtk go run cmd/mockrun/main.go` (观察日志确认 WASHER_HP 批量上料/卸料行为)
- [ ] **Step 2: 检查日志关键事件**
确认日志中出现:
- `dispatch actions` 包含 `load_to_machine, load_to_machine, startup_washer`2个候选时)
- `dispatch actions` 包含 `load_to_machine, startup_washer`1个候选时)
- 卸载/换料场景的 action 数组正确
- [ ] **Step 3: 最终 Commit**
```bash
rtk git commit -m "feat(washer): 高压清洗机批量上料/下料/换料完成"
```