587 lines
18 KiB
Markdown
587 lines
18 KiB
Markdown
# 高压清洗机批量上料/下料/换料 实施计划
|
||||
|
|
|
|||
|
|
> **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
|
|||
|
|
|
|||
|
|
// 第一个 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 时,走批量编排路径:
|
|||
|
|
|
|||
|
|
```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个 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**
|
|||
|
|
|
|||
|
|
```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): 高压清洗机批量上料/下料/换料完成"
|
|||
|
|
```
|