18 KiB
高压清洗机批量上料/下料/换料 实施计划
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 数组:
// HardwareWorker 硬件执行器接口
type HardwareWorker interface {
Execute(ctx context.Context, actions []RobotAction) error
}
- Step 2: 适配 RobotWorker.Execute
修改 internal/processor/robot_worker.go 中 Execute 方法,串行执行 action 数组:
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 中新增:
// 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 的代码块:
// 删除这段:
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
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 中新增方法,查找是否有第二个待清洗工件可以同时上料:
// 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 数组:
// 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 时,走批量编排路径:
// 在 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 数组:
// 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
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:
// buildWasherUnloadActions 构建 WASHER_HP 卸载/换料 action 数组
// 检查 batch 设备另一个槽位状态,按需追加 action
func (l *ProductionEventLoop) buildWasherUnloadActions(ctx context.Context, ct scheduler.CandidateTask, snap *RuntimeSnapshot) []action.RobotAction {
var actions []action.RobotAction
// 第一个 action(Unload 或 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 时,走批量编排路径:
// 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:
// 替换原来的 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
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_washeraction 无状态变更 -
任一 action 失败 → 整个批次失败,已成功的 action 状态保留
-
Step 2: 新增 handleSingleActionResult 辅助方法
将现有 handleWorkerResult 中单 action 的状态推进逻辑提取为独立方法:
// 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
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 中添加:
// 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
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个 Done,0个配对 →
Unload + Unload -
2个 Done,1个配对 →
Exchange + Unload + StartupWasher -
2个 Done,2个配对 →
Exchange + Exchange + StartupWasher -
1个 Done,0个配对 →
Unload -
1个 Done,1个配对 →
Exchange + Load + StartupWasher -
Step 3: 运行全部测试
Run: rtk go test ./internal/eventloop/... ./internal/processor/... -count=1
- Step 4: Commit
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
rtk git commit -m "feat(washer): 高压清洗机批量上料/下料/换料完成"