1284 lines
32 KiB
Markdown
1284 lines
32 KiB
Markdown
# Machine Actor 实现计划
|
||||
|
|
|
|||
|
|
> **For agentic workers:** REQUIRED: Use superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|||
|
|
|
|||
|
|
**Goal:** 用 Actor 模型(goroutine + channel)替换 Station/SignalWatcher/TempSlotAllocator,每个设备自治管理槽位状态。
|
|||
|
|
|
|||
|
|
**Architecture:** 新建 `internal/actor/` 包,包含 MachineActor、TempStoreActor、SignalRouter 和共享类型。EventLoop 移除 equipment_slot 写入逻辑,调度器从 Actor 快照读设备/暂存台状态。
|
|||
|
|
|
|||
|
|
**Tech Stack:** Go 1.25, ent ORM, go-zero, PostgreSQL
|
|||
|
|
|
|||
|
|
**Spec:** `docs/superpowers/specs/2026-05-16-machine-actor-design.md`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 文件结构
|
|||
|
|
|
|||
|
|
| 文件 | 操作 | 职责 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| `internal/actor/types.go` | 新建 | 共享类型(MachineMsg, MachineSnapshot, ActorStatus, SignalEvent) |
|
|||
|
|
| `internal/actor/signal_router.go` | 新建 | SignalRouter 集中 PLC 轮询 + 路由 |
|
|||
|
|
| `internal/actor/machine_actor.go` | 新建 | MachineActor 状态机 |
|
|||
|
|
| `internal/actor/machine_actor_test.go` | 新建 | MachineActor 单元测试 |
|
|||
|
|
| `internal/actor/tempstore_actor.go` | 新建 | TempStoreActor 暂存台管理 |
|
|||
|
|
| `internal/actor/tempstore_actor_test.go` | 新建 | TempStoreActor 单元测试 |
|
|||
|
|
| `internal/eventloop/dbstate.go` | 修改 | 新增 SetJobTempSlot / ClearJobTempSlot |
|
|||
|
|
| `internal/eventloop/loop.go` | 修改 | 移除 updateStationDone/batchMachines/inspectionMachines;handleMachineDone 精简 |
|
|||
|
|
| `internal/eventloop/scheduler_bridge.go` | 修改 | buildSystemState 改读 Actor 快照 |
|
|||
|
|
| `internal/eventloop/worker_dispatch.go` | 修改 | 上料/下料完成后通知 Actor |
|
|||
|
|
| `internal/svc/service_context.go` | 修改 | 用 Actor 替换 Station/SignalWatcher |
|
|||
|
|
| `internal/station/*.go` | 删除 | 保留 HandheldTool 接口移到新位置 |
|
|||
|
|
| `internal/processor/signal_watcher.go` | 删除 | 被 SignalRouter 替代 |
|
|||
|
|
| `internal/processor/replenisher.go` | 修改 | 暂存台查询改为读 TempStoreActor 快照 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Chunk 1: Actor 类型 + DBState 扩展
|
|||
|
|
|
|||
|
|
### Task 1.1: 定义 Actor 共享类型
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `internal/actor/types.go`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 创建 types.go**
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
package actor
|
|||
|
|
|
|||
|
|
import "time"
|
|||
|
|
|
|||
|
|
// ActorStatus 工站状态
|
|||
|
|
type ActorStatus string
|
|||
|
|
|
|||
|
|
const (
|
|||
|
|
ActorIdle ActorStatus = "Idle"
|
|||
|
|
ActorProcessing ActorStatus = "Processing"
|
|||
|
|
ActorFull ActorStatus = "Full"
|
|||
|
|
ActorWaiting ActorStatus = "Waiting"
|
|||
|
|
ActorFault ActorStatus = "Fault"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// SlotStatus 槽位状态
|
|||
|
|
type SlotStatus string
|
|||
|
|
|
|||
|
|
const (
|
|||
|
|
SlotEmpty SlotStatus = "Empty"
|
|||
|
|
SlotOccupied SlotStatus = "Occupied"
|
|||
|
|
SlotDone SlotStatus = "Done"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// MachineMsg 外部发给 Actor 的命令
|
|||
|
|
type MachineMsg struct {
|
|||
|
|
Type string // LOAD_COMPLETE / UNLOAD_COMPLETE / RESTORE / ALLOCATE / RELEASE
|
|||
|
|
JobID int
|
|||
|
|
SlotNo int
|
|||
|
|
Params map[string]any
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// MachineSnapshot 设备 Actor 只读快照
|
|||
|
|
type MachineSnapshot struct {
|
|||
|
|
ID int
|
|||
|
|
Type string
|
|||
|
|
Status ActorStatus
|
|||
|
|
Slots []SlotSnapshot
|
|||
|
|
UpdatedAt time.Time
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SlotSnapshot 槽位快照
|
|||
|
|
type SlotSnapshot struct {
|
|||
|
|
SlotNo int
|
|||
|
|
Status SlotStatus
|
|||
|
|
JobID int
|
|||
|
|
OccupiedAt time.Time
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TempStoreSnapshot 暂存台快照
|
|||
|
|
type TempStoreSnapshot struct {
|
|||
|
|
Capacity int
|
|||
|
|
Slots []int // slotNo-1 → jobID (0 = 空), 复制
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SignalEvent PLC 信号事件
|
|||
|
|
type SignalEvent struct {
|
|||
|
|
Type SignalType
|
|||
|
|
Value bool
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type SignalType int
|
|||
|
|
|
|||
|
|
const (
|
|||
|
|
SignalDone SignalType = iota
|
|||
|
|
SignalNG
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 验证编译**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
go build ./internal/actor/...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Task 1.2: DBState 新增暂存台 DB 方法
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `internal/eventloop/dbstate.go`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 添加 SetJobTempSlot / ClearJobTempSlot**
|
|||
|
|
|
|||
|
|
在 `internal/eventloop/dbstate.go` 末尾添加:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// SetJobTempSlot 设置 job.temp_slot_no
|
|||
|
|
func (d *DBState) SetJobTempSlot(ctx context.Context, jobID, slotNo int) error {
|
|||
|
|
_, err := d.client.Job.UpdateOneID(jobID).
|
|||
|
|
SetTempSlotNo(slotNo).
|
|||
|
|
AddVersion(1).
|
|||
|
|
Save(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("set job %d temp slot %d: %w", jobID, slotNo, err)
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ClearJobTempSlot 清除 job.temp_slot_no
|
|||
|
|
func (d *DBState) ClearJobTempSlot(ctx context.Context, jobID int) error {
|
|||
|
|
_, err := d.client.Job.UpdateOneID(jobID).
|
|||
|
|
ClearTempSlotNo().
|
|||
|
|
AddVersion(1).
|
|||
|
|
Save(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("clear job %d temp slot: %w", jobID, err)
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 验证编译和已有测试**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
go build ./...
|
|||
|
|
go test ./internal/eventloop/...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 提交**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add internal/actor/types.go internal/eventloop/dbstate.go
|
|||
|
|
git commit -m "feat: add Actor types and DBState temp slot methods"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Chunk 2: SignalRouter
|
|||
|
|
|
|||
|
|
### Task 2.1: 实现 SignalRouter
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `internal/actor/signal_router.go`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 实现 SignalRouter**
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
package actor
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"sync"
|
|||
|
|
|
|||
|
|
goplc "bjhardman.cn/bjhardman/goplc"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
type SignalRouter struct {
|
|||
|
|
plc goplc.Client
|
|||
|
|
mu sync.RWMutex
|
|||
|
|
watches map[int]*SignalWatch
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type SignalWatch struct {
|
|||
|
|
DoneAddr string
|
|||
|
|
NGAddr string
|
|||
|
|
Ch chan SignalEvent
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func NewSignalRouter(plc goplc.Client) *SignalRouter {
|
|||
|
|
return &SignalRouter{
|
|||
|
|
plc: plc,
|
|||
|
|
watches: make(map[int]*SignalWatch),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Watch 注册设备信号监听,返回 Actor 用于接收信号的 channel
|
|||
|
|
func (r *SignalRouter) Watch(machineID int, doneAddr, ngAddr string) chan SignalEvent {
|
|||
|
|
r.mu.Lock()
|
|||
|
|
defer r.mu.Unlock()
|
|||
|
|
ch := make(chan SignalEvent, 1)
|
|||
|
|
r.watches[machineID] = &SignalWatch{
|
|||
|
|
DoneAddr: doneAddr,
|
|||
|
|
NGAddr: ngAddr,
|
|||
|
|
Ch: ch,
|
|||
|
|
}
|
|||
|
|
return ch
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Run 轮询 PLC 信号,阻塞直到 ctx 取消
|
|||
|
|
func (r *SignalRouter) Run(ctx context.Context, interval time.Duration) {
|
|||
|
|
ticker := time.NewTicker(interval)
|
|||
|
|
defer ticker.Stop()
|
|||
|
|
|
|||
|
|
lastState := make(map[int]bool) // machineID → 上次Done值
|
|||
|
|
lastNG := make(map[int]bool) // -machineID → 上次NG值
|
|||
|
|
|
|||
|
|
for {
|
|||
|
|
select {
|
|||
|
|
case <-ctx.Done():
|
|||
|
|
return
|
|||
|
|
case <-ticker.C:
|
|||
|
|
r.poll(lastState, lastNG)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (r *SignalRouter) poll(lastState, lastNG map[int]bool) {
|
|||
|
|
if r.plc == nil || !r.plc.IsConnected() {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
r.mu.RLock()
|
|||
|
|
defer r.mu.RUnlock()
|
|||
|
|
|
|||
|
|
for machineID, w := range r.watches {
|
|||
|
|
if w.DoneAddr == "" {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
current, err := r.plc.ReadBool(w.DoneAddr)
|
|||
|
|
if err != nil {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
prev := lastState[machineID]
|
|||
|
|
lastState[machineID] = current
|
|||
|
|
|
|||
|
|
// 上升沿
|
|||
|
|
if current && !prev {
|
|||
|
|
r.sendNonBlock(w.Ch, SignalEvent{Type: SignalDone, Value: true})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NG 信号
|
|||
|
|
if w.NGAddr != "" {
|
|||
|
|
ng, err := r.plc.ReadBool(w.NGAddr)
|
|||
|
|
if err != nil {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
key := -machineID
|
|||
|
|
ngPrev := lastNG[key]
|
|||
|
|
lastNG[key] = ng
|
|||
|
|
if ng && !ngPrev {
|
|||
|
|
r.sendNonBlock(w.Ch, SignalEvent{Type: SignalNG, Value: true})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (r *SignalRouter) sendNonBlock(ch chan SignalEvent, evt SignalEvent) {
|
|||
|
|
select {
|
|||
|
|
case ch <- evt:
|
|||
|
|
default:
|
|||
|
|
// 缓冲区满,丢弃(表示上一事件未被消费)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 验证编译**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
go build ./internal/actor/...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Chunk 3: MachineActor
|
|||
|
|
|
|||
|
|
### Task 3.1: 实现 MachineActor
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `internal/actor/machine_actor.go`
|
|||
|
|
- Create: `internal/actor/machine_actor_test.go`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 写失败测试**
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// internal/actor/machine_actor_test.go
|
|||
|
|
package actor
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"testing"
|
|||
|
|
"time"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
func TestMachineActor_IdleToProcessing(t *testing.T) {
|
|||
|
|
cfg := MachineActorConfig{
|
|||
|
|
ID: 1,
|
|||
|
|
Type: "CNC",
|
|||
|
|
Capacity: 2,
|
|||
|
|
SignalCh: make(chan SignalEvent),
|
|||
|
|
}
|
|||
|
|
actor := NewMachineActor(cfg)
|
|||
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|||
|
|
defer cancel()
|
|||
|
|
go actor.Run(ctx)
|
|||
|
|
|
|||
|
|
// Load first job
|
|||
|
|
actor.Send(MachineMsg{Type: "LOAD_COMPLETE", JobID: 100, SlotNo: 1})
|
|||
|
|
time.Sleep(10 * time.Millisecond)
|
|||
|
|
|
|||
|
|
snap := actor.Snapshot()
|
|||
|
|
if snap.Status != ActorProcessing {
|
|||
|
|
t.Fatalf("expected Processing, got %s", snap.Status)
|
|||
|
|
}
|
|||
|
|
if snap.Slots[0].Status != SlotOccupied || snap.Slots[0].JobID != 100 {
|
|||
|
|
t.Fatalf("expected slot 1 Occupied with job 100, got %v", snap.Slots[0])
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestMachineActor_FullOnAllOccupied(t *testing.T) {
|
|||
|
|
cfg := MachineActorConfig{
|
|||
|
|
ID: 2,
|
|||
|
|
Type: "CNC",
|
|||
|
|
Capacity: 1,
|
|||
|
|
SignalCh: make(chan SignalEvent),
|
|||
|
|
}
|
|||
|
|
actor := NewMachineActor(cfg)
|
|||
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|||
|
|
defer cancel()
|
|||
|
|
go actor.Run(ctx)
|
|||
|
|
|
|||
|
|
actor.Send(MachineMsg{Type: "LOAD_COMPLETE", JobID: 200, SlotNo: 1})
|
|||
|
|
time.Sleep(10 * time.Millisecond)
|
|||
|
|
|
|||
|
|
snap := actor.Snapshot()
|
|||
|
|
if snap.Status != ActorFull {
|
|||
|
|
t.Fatalf("expected Full for 1-slot CNC, got %s", snap.Status)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestMachineActor_DoneSignal(t *testing.T) {
|
|||
|
|
signalCh := make(chan SignalEvent, 1)
|
|||
|
|
cfg := MachineActorConfig{
|
|||
|
|
ID: 3,
|
|||
|
|
Type: "CNC",
|
|||
|
|
Capacity: 1,
|
|||
|
|
SignalCh: signalCh,
|
|||
|
|
}
|
|||
|
|
actor := NewMachineActor(cfg)
|
|||
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|||
|
|
defer cancel()
|
|||
|
|
go actor.Run(ctx)
|
|||
|
|
|
|||
|
|
actor.Send(MachineMsg{Type: "LOAD_COMPLETE", JobID: 300, SlotNo: 1})
|
|||
|
|
time.Sleep(10 * time.Millisecond)
|
|||
|
|
|
|||
|
|
// Simulate PLC done signal
|
|||
|
|
signalCh <- SignalEvent{Type: SignalDone, Value: true}
|
|||
|
|
time.Sleep(10 * time.Millisecond)
|
|||
|
|
|
|||
|
|
snap := actor.Snapshot()
|
|||
|
|
if snap.Status != ActorWaiting {
|
|||
|
|
t.Fatalf("expected Waiting, got %s", snap.Status)
|
|||
|
|
}
|
|||
|
|
if snap.Slots[0].Status != SlotDone {
|
|||
|
|
t.Fatalf("expected slot Done, got %s", snap.Slots[0].Status)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestMachineActor_BatchDoneSignal(t *testing.T) {
|
|||
|
|
signalCh := make(chan SignalEvent, 1)
|
|||
|
|
cfg := MachineActorConfig{
|
|||
|
|
ID: 5,
|
|||
|
|
Type: "WASHER_HP",
|
|||
|
|
Capacity: 2,
|
|||
|
|
Batch: true,
|
|||
|
|
SignalCh: signalCh,
|
|||
|
|
}
|
|||
|
|
actor := NewMachineActor(cfg)
|
|||
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|||
|
|
defer cancel()
|
|||
|
|
go actor.Run(ctx)
|
|||
|
|
|
|||
|
|
actor.Send(MachineMsg{Type: "LOAD_COMPLETE", JobID: 400, SlotNo: 1})
|
|||
|
|
actor.Send(MachineMsg{Type: "LOAD_COMPLETE", JobID: 401, SlotNo: 2})
|
|||
|
|
time.Sleep(10 * time.Millisecond)
|
|||
|
|
|
|||
|
|
signalCh <- SignalEvent{Type: SignalDone, Value: true}
|
|||
|
|
time.Sleep(10 * time.Millisecond)
|
|||
|
|
|
|||
|
|
snap := actor.Snapshot()
|
|||
|
|
if snap.Slots[0].Status != SlotDone {
|
|||
|
|
t.Fatalf("expected slot 1 Done, got %s", snap.Slots[0].Status)
|
|||
|
|
}
|
|||
|
|
if snap.Slots[1].Status != SlotDone {
|
|||
|
|
t.Fatalf("expected slot 2 Done, got %s", snap.Slots[1].Status)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 运行测试验证失败**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
go test ./internal/actor/... -run TestMachineActor -v
|
|||
|
|
```
|
|||
|
|
Expected: FAIL (MachineActor not defined)
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 实现 MachineActor**
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// internal/actor/machine_actor.go
|
|||
|
|
package actor
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"sync/atomic"
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
"hougai/constants"
|
|||
|
|
"hougai/internal/eventbus"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
type MachineActorConfig struct {
|
|||
|
|
ID int
|
|||
|
|
Type string
|
|||
|
|
Capacity int
|
|||
|
|
Batch bool
|
|||
|
|
Inspection bool
|
|||
|
|
SignalCh chan SignalEvent
|
|||
|
|
DB interface { // DBState 的子集,避免循环依赖
|
|||
|
|
SetEquipmentSlot(ctx context.Context, equipmentID, slotNo int, expected, next constants.SlotStatus, jobID int) error
|
|||
|
|
}
|
|||
|
|
Bus eventbus.Bus
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type machineActor struct {
|
|||
|
|
id int
|
|||
|
|
typ string
|
|||
|
|
capacity int
|
|||
|
|
batch bool
|
|||
|
|
inspection bool
|
|||
|
|
signalCh chan SignalEvent
|
|||
|
|
msgCh chan MachineMsg
|
|||
|
|
slots []SlotStatus
|
|||
|
|
slotJobs []int
|
|||
|
|
slotOccupiedAt []time.Time
|
|||
|
|
status atomic.Value // ActorStatus
|
|||
|
|
db interface {
|
|||
|
|
SetEquipmentSlot(ctx context.Context, equipmentID, slotNo int, expected, next constants.SlotStatus, jobID int) error
|
|||
|
|
}
|
|||
|
|
bus eventbus.Bus
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func NewMachineActor(cfg MachineActorConfig) *machineActor {
|
|||
|
|
slots := make([]SlotStatus, cfg.Capacity)
|
|||
|
|
slotJobs := make([]int, cfg.Capacity)
|
|||
|
|
slotOccupiedAt := make([]time.Time, cfg.Capacity)
|
|||
|
|
for i := range slots {
|
|||
|
|
slots[i] = SlotEmpty
|
|||
|
|
}
|
|||
|
|
a := &machineActor{
|
|||
|
|
id: cfg.ID,
|
|||
|
|
typ: cfg.Type,
|
|||
|
|
capacity: cfg.Capacity,
|
|||
|
|
batch: cfg.Batch,
|
|||
|
|
inspection: cfg.Inspection,
|
|||
|
|
signalCh: cfg.SignalCh,
|
|||
|
|
msgCh: make(chan MachineMsg, 8),
|
|||
|
|
slots: slots,
|
|||
|
|
slotJobs: slotJobs,
|
|||
|
|
slotOccupiedAt: slotOccupiedAt,
|
|||
|
|
db: cfg.DB,
|
|||
|
|
bus: cfg.Bus,
|
|||
|
|
}
|
|||
|
|
a.status.Store(ActorIdle)
|
|||
|
|
return a
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (m *machineActor) ID() int { return m.id }
|
|||
|
|
func (m *machineActor) Type() string { return m.typ }
|
|||
|
|
|
|||
|
|
func (m *machineActor) Send(msg MachineMsg) {
|
|||
|
|
select {
|
|||
|
|
case m.msgCh <- msg:
|
|||
|
|
default:
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (m *machineActor) Snapshot() MachineSnapshot {
|
|||
|
|
slots := make([]SlotSnapshot, m.capacity)
|
|||
|
|
for i := range m.slots {
|
|||
|
|
slots[i] = SlotSnapshot{
|
|||
|
|
SlotNo: i + 1,
|
|||
|
|
Status: m.slots[i],
|
|||
|
|
JobID: m.slotJobs[i],
|
|||
|
|
OccupiedAt: m.slotOccupiedAt[i],
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return MachineSnapshot{
|
|||
|
|
ID: m.id,
|
|||
|
|
Type: m.typ,
|
|||
|
|
Status: m.status.Load().(ActorStatus),
|
|||
|
|
Slots: slots,
|
|||
|
|
UpdatedAt: time.Now(),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (m *machineActor) Run(ctx context.Context) {
|
|||
|
|
for {
|
|||
|
|
select {
|
|||
|
|
case msg := <-m.msgCh:
|
|||
|
|
m.handleMessage(ctx, msg)
|
|||
|
|
case evt := <-m.signalCh:
|
|||
|
|
m.handleSignal(ctx, evt)
|
|||
|
|
case <-ctx.Done():
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (m *machineActor) handleMessage(ctx context.Context, msg MachineMsg) {
|
|||
|
|
switch msg.Type {
|
|||
|
|
case "LOAD_COMPLETE":
|
|||
|
|
m.slots[msg.SlotNo-1] = SlotOccupied
|
|||
|
|
m.slotJobs[msg.SlotNo-1] = msg.JobID
|
|||
|
|
m.slotOccupiedAt[msg.SlotNo-1] = time.Now()
|
|||
|
|
m.updateStatus()
|
|||
|
|
if m.db != nil {
|
|||
|
|
m.db.SetEquipmentSlot(ctx, m.id, msg.SlotNo, constants.SlotStatus_Empty, constants.SlotStatus_Occupied, msg.JobID)
|
|||
|
|
}
|
|||
|
|
case "UNLOAD_COMPLETE":
|
|||
|
|
if m.db != nil {
|
|||
|
|
m.db.SetEquipmentSlot(ctx, m.id, msg.SlotNo, constants.SlotStatus_Done, constants.SlotStatus_Empty, 0)
|
|||
|
|
}
|
|||
|
|
m.slots[msg.SlotNo-1] = SlotEmpty
|
|||
|
|
m.slotJobs[msg.SlotNo-1] = 0
|
|||
|
|
m.slotOccupiedAt[msg.SlotNo-1] = time.Time{}
|
|||
|
|
m.updateStatus()
|
|||
|
|
case "RESTORE":
|
|||
|
|
m.slots[msg.SlotNo-1] = parseSlotStatus(msg.Params["status"])
|
|||
|
|
m.slotJobs[msg.SlotNo-1] = msg.JobID
|
|||
|
|
m.updateStatus()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (m *machineActor) handleSignal(ctx context.Context, evt SignalEvent) {
|
|||
|
|
if evt.Type == SignalNG && evt.Value && m.inspection {
|
|||
|
|
if m.bus != nil {
|
|||
|
|
m.bus.Publish(eventbus.Event{
|
|||
|
|
Type: eventbus.EventInspectionResult,
|
|||
|
|
Data: map[string]any{"machineId": m.id, "pass": false},
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if evt.Type == SignalDone && evt.Value {
|
|||
|
|
if m.inspection {
|
|||
|
|
for i := range m.slots {
|
|||
|
|
if m.slots[i] == SlotOccupied {
|
|||
|
|
m.slots[i] = SlotDone
|
|||
|
|
if m.db != nil {
|
|||
|
|
m.db.SetEquipmentSlot(ctx, m.id, i+1, constants.SlotStatus_Occupied, constants.SlotStatus_Done, m.slotJobs[i])
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
m.updateStatus()
|
|||
|
|
if m.bus != nil {
|
|||
|
|
m.bus.Publish(eventbus.Event{
|
|||
|
|
Type: eventbus.EventInspectionResult,
|
|||
|
|
Data: map[string]any{"machineId": m.id, "pass": true},
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var doneSlots []int
|
|||
|
|
if m.batch {
|
|||
|
|
for i := range m.slots {
|
|||
|
|
if m.slots[i] == SlotOccupied {
|
|||
|
|
m.slots[i] = SlotDone
|
|||
|
|
doneSlots = append(doneSlots, i+1)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
slotNo := m.findFirstOccupied()
|
|||
|
|
if slotNo > 0 {
|
|||
|
|
m.slots[slotNo-1] = SlotDone
|
|||
|
|
doneSlots = append(doneSlots, slotNo)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
m.updateStatus()
|
|||
|
|
for _, slotNo := range doneSlots {
|
|||
|
|
if m.db != nil {
|
|||
|
|
m.db.SetEquipmentSlot(ctx, m.id, slotNo, constants.SlotStatus_Occupied, constants.SlotStatus_Done, m.slotJobs[slotNo-1])
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if m.bus != nil {
|
|||
|
|
for _, slotNo := range doneSlots {
|
|||
|
|
m.bus.Publish(eventbus.Event{
|
|||
|
|
Type: eventbus.EventMachineDone,
|
|||
|
|
Data: map[string]any{"machineId": m.id, "slotNo": slotNo, "jobId": m.slotJobs[slotNo-1]},
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (m *machineActor) findFirstOccupied() int {
|
|||
|
|
earliest := time.Time{}
|
|||
|
|
earliestSlot := 0
|
|||
|
|
for i, s := range m.slots {
|
|||
|
|
if s != SlotOccupied {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
if t := m.slotOccupiedAt[i]; earliestSlot == 0 || t.Before(earliest) {
|
|||
|
|
earliest = t
|
|||
|
|
earliestSlot = i + 1
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return earliestSlot
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (m *machineActor) updateStatus() {
|
|||
|
|
hasEmpty := false
|
|||
|
|
hasOccupied := false
|
|||
|
|
hasDone := false
|
|||
|
|
for _, s := range m.slots {
|
|||
|
|
switch s {
|
|||
|
|
case SlotEmpty:
|
|||
|
|
hasEmpty = true
|
|||
|
|
case SlotOccupied:
|
|||
|
|
hasOccupied = true
|
|||
|
|
case SlotDone:
|
|||
|
|
hasDone = true
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var s ActorStatus
|
|||
|
|
switch {
|
|||
|
|
case !hasOccupied && !hasDone:
|
|||
|
|
s = ActorIdle
|
|||
|
|
case hasDone && !hasOccupied:
|
|||
|
|
s = ActorWaiting
|
|||
|
|
case !hasEmpty:
|
|||
|
|
s = ActorFull
|
|||
|
|
default:
|
|||
|
|
s = ActorProcessing
|
|||
|
|
}
|
|||
|
|
m.status.Store(s)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func parseSlotStatus(s string) SlotStatus {
|
|||
|
|
if s == "OCCUPIED" {
|
|||
|
|
return SlotOccupied
|
|||
|
|
}
|
|||
|
|
if s == "DONE" {
|
|||
|
|
return SlotDone
|
|||
|
|
}
|
|||
|
|
return SlotEmpty
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: 运行测试**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
go test ./internal/actor/... -run TestMachineActor -v
|
|||
|
|
```
|
|||
|
|
Expected: PASS (4 tests)
|
|||
|
|
|
|||
|
|
- [ ] **Step 5: 提交**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add internal/actor/machine_actor.go internal/actor/machine_actor_test.go
|
|||
|
|
git commit -m "feat: add MachineActor with state machine and PLC signal handling"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Chunk 4: TempStoreActor
|
|||
|
|
|
|||
|
|
### Task 4.1: 实现 TempStoreActor
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `internal/actor/tempstore_actor.go`
|
|||
|
|
- Create: `internal/actor/tempstore_actor_test.go`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 写失败测试**
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// internal/actor/tempstore_actor_test.go
|
|||
|
|
package actor
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"testing"
|
|||
|
|
"time"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
func TestTempStoreActor_AllocateRelease(t *testing.T) {
|
|||
|
|
actor := NewTempStoreActor(8, nil)
|
|||
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|||
|
|
defer cancel()
|
|||
|
|
go actor.Run(ctx)
|
|||
|
|
|
|||
|
|
actor.Send(MachineMsg{Type: "ALLOCATE", JobID: 100})
|
|||
|
|
time.Sleep(10 * time.Millisecond)
|
|||
|
|
|
|||
|
|
snap := actor.Snapshot()
|
|||
|
|
found := false
|
|||
|
|
for slotNo, jobID := range snap.Slots {
|
|||
|
|
if jobID == 100 {
|
|||
|
|
if slotNo+1 < 1 || slotNo+1 > 8 {
|
|||
|
|
t.Fatalf("invalid slot %d", slotNo+1)
|
|||
|
|
}
|
|||
|
|
found = true
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if !found {
|
|||
|
|
t.Fatalf("job 100 not allocated in temp store")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
actor.Send(MachineMsg{Type: "RELEASE", JobID: 100})
|
|||
|
|
time.Sleep(10 * time.Millisecond)
|
|||
|
|
|
|||
|
|
snap = actor.Snapshot()
|
|||
|
|
for _, jobID := range snap.Slots {
|
|||
|
|
if jobID == 100 {
|
|||
|
|
t.Fatalf("job 100 should have been released")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestTempStoreActor_Full(t *testing.T) {
|
|||
|
|
actor := NewTempStoreActor(2, nil)
|
|||
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|||
|
|
defer cancel()
|
|||
|
|
go actor.Run(ctx)
|
|||
|
|
|
|||
|
|
actor.Send(MachineMsg{Type: "ALLOCATE", JobID: 1})
|
|||
|
|
actor.Send(MachineMsg{Type: "ALLOCATE", JobID: 2})
|
|||
|
|
time.Sleep(10 * time.Millisecond)
|
|||
|
|
|
|||
|
|
snap := actor.Snapshot()
|
|||
|
|
free := 0
|
|||
|
|
for _, jobID := range snap.Slots {
|
|||
|
|
if jobID == 0 {
|
|||
|
|
free++
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if free != 0 {
|
|||
|
|
t.Fatalf("expected 0 free slots, got %d", free)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 运行测试验证失败**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
go test ./internal/actor/... -run TestTempStore -v
|
|||
|
|
```
|
|||
|
|
Expected: FAIL
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 实现 TempStoreActor**
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// internal/actor/tempstore_actor.go
|
|||
|
|
package actor
|
|||
|
|
|
|||
|
|
import "context"
|
|||
|
|
|
|||
|
|
type TempStoreActor struct {
|
|||
|
|
capacity int
|
|||
|
|
slots []int
|
|||
|
|
msgCh chan MachineMsg
|
|||
|
|
db interface {
|
|||
|
|
SetJobTempSlot(ctx context.Context, jobID, slotNo int) error
|
|||
|
|
ClearJobTempSlot(ctx context.Context, jobID int) error
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func NewTempStoreActor(capacity int, db interface {
|
|||
|
|
SetJobTempSlot(ctx context.Context, jobID, slotNo int) error
|
|||
|
|
ClearJobTempSlot(ctx context.Context, jobID int) error
|
|||
|
|
}) *TempStoreActor {
|
|||
|
|
return &TempStoreActor{
|
|||
|
|
capacity: capacity,
|
|||
|
|
slots: make([]int, capacity),
|
|||
|
|
msgCh: make(chan MachineMsg, 8),
|
|||
|
|
db: db,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (t *TempStoreActor) Send(msg MachineMsg) {
|
|||
|
|
select {
|
|||
|
|
case t.msgCh <- msg:
|
|||
|
|
default:
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (t *TempStoreActor) Snapshot() TempStoreSnapshot {
|
|||
|
|
slots := make([]int, t.capacity)
|
|||
|
|
copy(slots, t.slots)
|
|||
|
|
return TempStoreSnapshot{
|
|||
|
|
Capacity: t.capacity,
|
|||
|
|
Slots: slots,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (t *TempStoreActor) Run(ctx context.Context) {
|
|||
|
|
for {
|
|||
|
|
select {
|
|||
|
|
case msg := <-t.msgCh:
|
|||
|
|
switch msg.Type {
|
|||
|
|
case "ALLOCATE":
|
|||
|
|
t.allocate(ctx, msg.JobID)
|
|||
|
|
case "RELEASE":
|
|||
|
|
t.release(ctx, msg.JobID)
|
|||
|
|
case "RESTORE":
|
|||
|
|
t.slots[msg.SlotNo-1] = msg.JobID
|
|||
|
|
}
|
|||
|
|
case <-ctx.Done():
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (t *TempStoreActor) allocate(ctx context.Context, jobID int) {
|
|||
|
|
for i := range t.slots {
|
|||
|
|
if t.slots[i] == 0 {
|
|||
|
|
t.slots[i] = jobID
|
|||
|
|
if t.db != nil {
|
|||
|
|
t.db.SetJobTempSlot(ctx, jobID, i+1)
|
|||
|
|
}
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (t *TempStoreActor) release(ctx context.Context, jobID int) {
|
|||
|
|
for i := range t.slots {
|
|||
|
|
if t.slots[i] == jobID {
|
|||
|
|
t.slots[i] = 0
|
|||
|
|
if t.db != nil {
|
|||
|
|
t.db.ClearJobTempSlot(ctx, jobID)
|
|||
|
|
}
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: 运行测试**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
go test ./internal/actor/... -run TestTempStore -v
|
|||
|
|
```
|
|||
|
|
Expected: PASS (2 tests)
|
|||
|
|
|
|||
|
|
- [ ] **Step 5: 提交**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add internal/actor/tempstore_actor.go internal/actor/tempstore_actor_test.go
|
|||
|
|
git commit -m "feat: add TempStoreActor for temp slot allocation"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Chunk 5: EventLoop 精简
|
|||
|
|
|
|||
|
|
### Task 5.1: 更新 ProductionEventLoop 结构
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `internal/eventloop/loop.go`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 替换 struct 字段**
|
|||
|
|
|
|||
|
|
将 `registry`, `batchMachines`, `inspectionMachines` 替换为:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
machineActors map[int]actor.MachineActor // 设备 Actor map
|
|||
|
|
tempStoreActor *actor.TempStoreActor // 暂存台 Actor
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
更新 `NewProductionEventLoop` 签名,接收 `machineActors` 和 `tempStoreActor` 替代旧的三个参数。
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 删除 `updateStationDone` 方法**
|
|||
|
|
|
|||
|
|
删除 `loop.go:363-386` 的 `updateStationDone` 方法。
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 精简 `handleMachineDone`**
|
|||
|
|
|
|||
|
|
移除 DB equipment_slot 更新(Actor 已写)、移除 `updateStationDone` 调用、移除 batch/non-batch slot 查找。精简为:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func (l *ProductionEventLoop) handleMachineDone(ctx context.Context, msg EventLoopMessage) {
|
|||
|
|
machineID := intFromPayload(msg.Payload, "machineId")
|
|||
|
|
if machineID == 0 {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
// 检测设备走 handleInspectionDone
|
|||
|
|
if l.inspectionActors[machineID] {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
slotNo := intFromPayload(msg.Payload, "slotNo")
|
|||
|
|
jobID := intFromPayload(msg.Payload, "jobId")
|
|||
|
|
|
|||
|
|
job, err := l.entClient.Job.Get(ctx, jobID)
|
|||
|
|
if err != nil || job.Status != constants.JobStatus_Processing {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if l.isOperationStep(jobID) {
|
|||
|
|
l.db.SetJobWaitingUnload(ctx, jobID)
|
|||
|
|
} else {
|
|||
|
|
l.db.AdvanceStep(ctx, jobID, nil, "", "")
|
|||
|
|
l.entClient.Job.UpdateOneID(jobID).
|
|||
|
|
SetStatus(constants.JobStatus_WaitingUnload).
|
|||
|
|
Save(ctx)
|
|||
|
|
}
|
|||
|
|
if l.jobOps != nil {
|
|||
|
|
l.jobOps.WakeJob(jobID)
|
|||
|
|
}
|
|||
|
|
l.trySchedule(ctx)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: 处理 Worker 结果时通知 Actor**
|
|||
|
|
|
|||
|
|
在 `handleWorkerResult` 的 Load 成功路径添加:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// 通知 Actor 上料完成
|
|||
|
|
actor, ok := l.machineActors[targetID]
|
|||
|
|
if ok {
|
|||
|
|
actor.Send(actor.MachineMsg{
|
|||
|
|
Type: "LOAD_COMPLETE",
|
|||
|
|
JobID: jobID,
|
|||
|
|
SlotNo: slotNo,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
在 Unload 成功路径添加:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// 通知 Actor 下料完成
|
|||
|
|
actor, ok := l.machineActors[targetID]
|
|||
|
|
if ok {
|
|||
|
|
actor.Send(actor.MachineMsg{
|
|||
|
|
Type: "UNLOAD_COMPLETE",
|
|||
|
|
JobID: jobID,
|
|||
|
|
SlotNo: slotNo,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 5: 验证编译**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
go build ./internal/eventloop/...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Task 5.2: 更新调度桥接
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `internal/eventloop/scheduler_bridge.go`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 更新 `buildSystemState`**
|
|||
|
|
|
|||
|
|
改为从 Actor 快照读:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func (l *ProductionEventLoop) buildSystemState(ctx context.Context) scheduler.SystemState {
|
|||
|
|
state := scheduler.SystemState{
|
|||
|
|
MachineBusy: make(map[int]bool),
|
|||
|
|
MachineHasJob: make(map[int]int),
|
|||
|
|
TempSlotJobs: make(map[int]int),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for _, a := range l.machineActors {
|
|||
|
|
snap := a.Snapshot()
|
|||
|
|
full := snap.Status == actor.ActorFull || snap.Status == actor.ActorWaiting
|
|||
|
|
state.MachineBusy[snap.ID] = full
|
|||
|
|
for _, slot := range snap.Slots {
|
|||
|
|
if slot.JobID > 0 && slot.Status != actor.SlotEmpty {
|
|||
|
|
state.MachineHasJob[snap.ID] = slot.JobID
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 暂存台
|
|||
|
|
if l.tempStoreActor != nil {
|
|||
|
|
tsSnap := l.tempStoreActor.Snapshot()
|
|||
|
|
occupied := 0
|
|||
|
|
for slotNo, jobID := range tsSnap.Slots {
|
|||
|
|
if jobID > 0 {
|
|||
|
|
state.TempSlotJobs[slotNo+1] = jobID
|
|||
|
|
occupied++
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
state.TempSlotFree = tsSnap.Capacity - occupied
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ... 后续逻辑不变(OrderPaused, JobSuspended)
|
|||
|
|
return state
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 更新 `findIdleMachine`**
|
|||
|
|
|
|||
|
|
改为从 Actor 快照查:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func (l *ProductionEventLoop) findIdleMachine(resourceType string, productTypeID int) int {
|
|||
|
|
for _, a := range l.machineActors {
|
|||
|
|
snap := a.Snapshot()
|
|||
|
|
if snap.Type != resourceType {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
if snap.Status == actor.ActorFault {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
// 有空槽位即可接受
|
|||
|
|
for _, slot := range snap.Slots {
|
|||
|
|
if slot.Status == actor.SlotEmpty {
|
|||
|
|
return snap.ID
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return 0
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 移除 `findDoneJobIDsOnMachine` DB 查询**
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func (l *ProductionEventLoop) findDoneJobIDsOnMachine(ctx context.Context, machineID int) []int {
|
|||
|
|
a, ok := l.machineActors[machineID]
|
|||
|
|
if !ok {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
snap := a.Snapshot()
|
|||
|
|
var ids []int
|
|||
|
|
for _, slot := range snap.Slots {
|
|||
|
|
if slot.Status == actor.SlotDone && slot.JobID > 0 {
|
|||
|
|
ids = append(ids, slot.JobID)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return ids
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: 验证编译和已有测试**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
go build ./...
|
|||
|
|
go test ./internal/eventloop/... -v
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 5: 提交**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add internal/eventloop/
|
|||
|
|
git commit -m "refactor: EventLoop reads from Actor snapshots, removes equipment_slot writes"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Chunk 6: ServiceContext 组装
|
|||
|
|
|
|||
|
|
### Task 6.1: 更新服务初始化
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `internal/svc/service_context.go`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 创建 Actor 并在 ServiceContext 中替换 Station/SignalWatcher**
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// 替换 BuildProductionLine 调用为 BuildMachineActors
|
|||
|
|
import "hougai/internal/actor"
|
|||
|
|
|
|||
|
|
// 创建 SignalRouter
|
|||
|
|
signalRouter := actor.NewSignalRouter(plcManager.GetPlc())
|
|||
|
|
|
|||
|
|
// 从 DB 构建信号映射并注册
|
|||
|
|
doneSignals, ngSignals := buildDoneSignalMaps(ctx, entClient)
|
|||
|
|
for machineID, addr := range doneSignals {
|
|||
|
|
ngAddr := ngSignals[machineID]
|
|||
|
|
signalRouter.Watch(machineID, addr, ngAddr)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 创建 MachineActor
|
|||
|
|
machineActors := make(map[int]actor.MachineActor)
|
|||
|
|
equipments, _ := entClient.Equipment.Query().WithEquipmentType().All(ctx)
|
|||
|
|
for _, eq := range equipments {
|
|||
|
|
cfg := actor.MachineActorConfig{
|
|||
|
|
ID: eq.ID,
|
|||
|
|
Type: eq.Edges.EquipmentType.Code,
|
|||
|
|
Capacity: eq.SlotCount,
|
|||
|
|
Batch: eq.Batch,
|
|||
|
|
Inspection: eq.Edges.EquipmentType.Code == "INSPECTION" || eq.Edges.EquipmentType.Code == "SAMPLING",
|
|||
|
|
SignalCh: signalRouter.Watch(eq.ID, doneSignals[eq.ID], ngSignals[eq.ID]),
|
|||
|
|
DB: dbState,
|
|||
|
|
Bus: eventBus,
|
|||
|
|
}
|
|||
|
|
machineActors[eq.ID] = actor.NewMachineActor(cfg)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 创建 TempStoreActor
|
|||
|
|
tempStoreActor := actor.NewTempStoreActor(dbState.TempSlotCapacity(ctx), dbState)
|
|||
|
|
|
|||
|
|
// 启动 SignalRouter
|
|||
|
|
go signalRouter.Run(ctx, 1*time.Second)
|
|||
|
|
|
|||
|
|
// 启动所有 Actor
|
|||
|
|
for _, a := range machineActors {
|
|||
|
|
go a.Run(ctx)
|
|||
|
|
}
|
|||
|
|
go tempStoreActor.Run(ctx)
|
|||
|
|
|
|||
|
|
// 恢复 Actor 状态
|
|||
|
|
dbState.RecoverMachineActors(ctx, machineActors, tempStoreActor)
|
|||
|
|
|
|||
|
|
// 更新 NewProductionEventLoop 调用
|
|||
|
|
eventLoop := eventloop.NewProductionEventLoop(entClient, worker, machineActors, tempStoreActor, sched)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 移除 Station 初始化**
|
|||
|
|
|
|||
|
|
删除 `BuildProductionLine`、`RestoreStationSlots`、`configureCNCProductTypes` 调用。
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 移除 SignalWatcher 初始化**
|
|||
|
|
|
|||
|
|
删除 `NewSignalWatcher` 调用和回调绑定。
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: 移除 queryBatchMachines / queryInspectionMachines**
|
|||
|
|
|
|||
|
|
删除这些辅助函数,逻辑已内化到 Actor 配置中。
|
|||
|
|
|
|||
|
|
- [ ] **Step 5: 验证编译**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
go build ./...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 6: 提交**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add internal/svc/service_context.go
|
|||
|
|
git commit -m "refactor: wire Actors into ServiceContext, remove Station/SignalWatcher"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Chunk 7: 清理旧代码
|
|||
|
|
|
|||
|
|
### Task 7.1: 删除 Station 和 SignalWatcher
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 删除文件**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
rm internal/station/base.go
|
|||
|
|
rm internal/station/cnc.go
|
|||
|
|
rm internal/station/cleaning.go
|
|||
|
|
rm internal/station/washer.go
|
|||
|
|
rm internal/station/inspection.go
|
|||
|
|
rm internal/station/deburr.go
|
|||
|
|
rm internal/station/sampling.go
|
|||
|
|
rm internal/station/interface.go # Station 接口(保留 HandheldTool 到 action 包)
|
|||
|
|
rm internal/station/registry.go
|
|||
|
|
rm internal/station/helpers.go
|
|||
|
|
rm internal/station/scanner.go
|
|||
|
|
rm internal/station/laser_marker.go
|
|||
|
|
# 保留: internal/station/interface_test.go 中 HandheldTool 相关
|
|||
|
|
rm internal/processor/signal_watcher.go
|
|||
|
|
rm internal/processor/step_timeout.go
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 将 HandheldTool 接口移到 action 包**
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// internal/action/tool.go (或保留在 station 包的最小文件中)
|
|||
|
|
type HandheldTool interface {
|
|||
|
|
ID() string
|
|||
|
|
Type() string
|
|||
|
|
Name() string
|
|||
|
|
Execute(cmd StationCommand) error
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
更新所有 import 路径。
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 清理 processor 包引用**
|
|||
|
|
|
|||
|
|
移除 `internal/processor/job_processor.go` 中的 `registry *station.StationRegistry` 字段。移除 `internal/processor/robot_worker.go` 中的 registry 引用(上料卸载涉及 station 的逻辑改为通过 Actor 发送消息)。
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: 更新 Replenisher**
|
|||
|
|
|
|||
|
|
`internal/processor/replenisher.go` 中的 `findFreeTempSlot` / `freeSlotsFromDB` 改为从 TempStoreActor 快照查。
|
|||
|
|
|
|||
|
|
- [ ] **Step 5: 验证全量编译**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
go build ./...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 6: 运行所有测试**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
go test ./internal/eventloop/... ./internal/actor/... ./internal/scheduler/... ./internal/processor/...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 7: 提交**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add -A
|
|||
|
|
git commit -m "refactor: remove Station/SignalWatcher, migrate to Actor model"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Chunk 8: 端到端验证
|
|||
|
|
|
|||
|
|
### Task 8.1: Mockrun 集成验证
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 确认 Mock.Enable=true**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
grep "Enable:" etc/spherical-api.yaml
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 运行端到端测试**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
go run cmd/mockrun/main.go -jobs 2 -timeout 5m
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Expected: 2 个工件全部完成,无超时、无报错。
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 修复任何集成问题**
|
|||
|
|
|
|||
|
|
根据 mockrun 输出修复 Actor 与 EventLoop 之间的消息传递、事件路由等问题。
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: 最终提交**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add -A
|
|||
|
|
git commit -m "fix: Actor integration issues from mockrun verification"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 回滚策略
|
|||
|
|
|
|||
|
|
如果遇到不可逾越的阻塞,所有变更在 `actor模型` 分支上,可随时回到 master:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git checkout master
|
|||
|
|
```
|