279 lines
8.1 KiB
Go
279 lines
8.1 KiB
Go
package svc
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"bj_power_mes/constants"
|
|
"bj_power_mes/ent"
|
|
"bj_power_mes/internal/alarm"
|
|
"bj_power_mes/internal/config"
|
|
"bj_power_mes/internal/db"
|
|
"bj_power_mes/internal/eventbus"
|
|
"bj_power_mes/internal/eventlog"
|
|
"bj_power_mes/internal/mock"
|
|
"bj_power_mes/internal/plc"
|
|
"bj_power_mes/internal/preload"
|
|
"bj_power_mes/internal/processor"
|
|
"bj_power_mes/internal/processor/eventloop"
|
|
"bj_power_mes/internal/processor/scheduler"
|
|
"bj_power_mes/internal/processor/tool"
|
|
"bj_power_mes/internal/robot"
|
|
"bj_power_mes/internal/sse"
|
|
|
|
"bjhardman.cn/bjhardman/mom"
|
|
"github.com/zeromicro/go-zero/core/stores/redis"
|
|
)
|
|
|
|
type ServiceContext struct {
|
|
Config config.Config
|
|
RedisClient *redis.Redis
|
|
EntClient *ent.Client
|
|
DB *sql.DB
|
|
SSEHandler *sse.Handler
|
|
PLCManager *plc.Manager
|
|
RobotManager *robot.Manager
|
|
RecipeLoader *processor.RecipeLoader
|
|
OrderProcessor processor.OrderProcessorInterface
|
|
EventLoop *eventloop.ProductionEventLoop
|
|
EventBus eventbus.Bus
|
|
EventLogWriter *eventlog.EventLogWriter
|
|
SSEBridge *sse.EventBridge
|
|
RecoveryCoordinator *eventloop.RecoveryCoordinator
|
|
Mom *mom.Mom
|
|
EquipUploader *processor.EquipUploadService
|
|
}
|
|
|
|
func NewServiceContext(c config.Config) *ServiceContext {
|
|
rc := redis.MustNewRedis(c.Redis)
|
|
entClient, dbConn := db.MustNewDB(c.Database)
|
|
//tools.EnsureDataInitialized(context.Background(), entClient)
|
|
|
|
preload.Init(entClient)
|
|
preload.InitProductTypeCategories(entClient)
|
|
|
|
// 事件总线:始终 LocalBus(单进程模式,不再支持 RedisBus)
|
|
bus := eventbus.NewLocalBus()
|
|
slog.Info("svc: using LocalBus for event bus")
|
|
|
|
alarmSvc := alarm.NewService(entClient, bus)
|
|
|
|
sseHandler := sse.NewHandler()
|
|
|
|
var plcManager *plc.Manager
|
|
if c.Mock.Enable {
|
|
plcManager = plc.NewManager(mock.NewMockPLC())
|
|
} else {
|
|
plcManager = plc.Build(c.Plc, bus)
|
|
}
|
|
|
|
// Mock 模式:清空旧数据,避免恢复逻辑干扰
|
|
if c.Mock.MockRun {
|
|
ctx := context.Background()
|
|
entClient.Job.Delete().ExecX(ctx)
|
|
entClient.WorkOrder.Delete().ExecX(ctx)
|
|
slots, _ := entClient.EquipmentSlot.Query().All(ctx)
|
|
for _, s := range slots {
|
|
entClient.EquipmentSlot.UpdateOneID(s.ID).
|
|
SetStatus(constants.SlotStatus_Empty).
|
|
ClearCurrentJobId().
|
|
ClearOccupiedAt().
|
|
ExecX(ctx)
|
|
}
|
|
dockSlots := entClient.DockSlot.Query().AllX(ctx)
|
|
for _, d := range dockSlots {
|
|
entClient.DockSlot.UpdateOneID(d.ID).
|
|
SetStatus(0).
|
|
ClearCurrentJobId().
|
|
ClearWorkOrderId().
|
|
ClearWorkpieceTypeId().
|
|
ExecX(ctx)
|
|
}
|
|
slog.Info("svc: mock mode, cleared old data")
|
|
}
|
|
|
|
robotManager := robot.MustNewManager(entClient, plcManager, sseHandler, alarmSvc)
|
|
|
|
robotCtrl, robotExist := robotManager.GetRobot()
|
|
if !robotExist {
|
|
slog.Warn("svc: robot not available, stations will run without robot actions")
|
|
}
|
|
|
|
dbState := eventloop.NewDBState(entClient)
|
|
scanner := tool.NewScannerTool(robotManager)
|
|
marker := tool.NewMarkerTool(robotManager)
|
|
recipeLoader := processor.NewRecipeLoader(entClient)
|
|
sched := scheduler.DefaultScheduler()
|
|
|
|
// 提前声明供 Actor 的 OnDone/OnInspect 闭包捕获(EventLoop 随后创建)
|
|
var eventLoop *eventloop.ProductionEventLoop
|
|
|
|
line, err := processor.BuildProductionLine(context.Background(), entClient, dbState, plcManager.GetPlc(), robotCtrl,
|
|
func(machineID, slotNo, jobID int) {
|
|
if eventLoop != nil {
|
|
eventLoop.Send(eventloop.EventLoopMessage{
|
|
ID: fmt.Sprintf("actor-done-%d-%d", machineID, time.Now().UnixNano()),
|
|
Type: eventloop.EvtMachineDone,
|
|
Payload: map[string]any{
|
|
"machineId": machineID,
|
|
"slotNo": slotNo,
|
|
"jobId": jobID,
|
|
},
|
|
})
|
|
}
|
|
},
|
|
func(machineID int, pass bool) {
|
|
if eventLoop != nil {
|
|
eventLoop.Send(eventloop.EventLoopMessage{
|
|
ID: fmt.Sprintf("actor-insp-%d-%d", machineID, time.Now().UnixNano()),
|
|
Type: eventloop.EvtMachineDone,
|
|
Payload: map[string]any{
|
|
"machineId": machineID,
|
|
"inspectionPass": pass,
|
|
},
|
|
})
|
|
}
|
|
},
|
|
func(machineID int) {
|
|
if eventLoop != nil {
|
|
eventLoop.SendScheduleTick()
|
|
}
|
|
},
|
|
sseHandler,
|
|
)
|
|
if err != nil {
|
|
slog.Error("svc: build production line failed", "error", err)
|
|
return nil
|
|
}
|
|
|
|
orderProcessor := processor.NewJobProcessor(
|
|
entClient, line.Actors, bus, scanner, marker, robotCtrl, robotManager, line.TempStore,
|
|
)
|
|
worker := processor.NewRobotWorker(entClient, robotCtrl, line.Actors, line.Stations, scanner, marker)
|
|
|
|
// MOM 客户端初始化(必须在 EventLoop 构造之前)
|
|
var momClient *mom.Mom
|
|
if c.Mom.Enable {
|
|
var err error
|
|
momClient, err = mom.New(c.Mom.MomConf)
|
|
if err != nil {
|
|
slog.Error("svc: mom init failed", "error", err)
|
|
}
|
|
}
|
|
|
|
// DockStateReader adapter
|
|
var dockReader eventloop.DockStateReader
|
|
if robotCtrl != nil {
|
|
dockReader = dockStateAdapter{ctrl: robotCtrl}
|
|
}
|
|
|
|
momBiz := eventloop.MomBizConfig{
|
|
InstoreType: c.Mom.InstoreType,
|
|
}
|
|
|
|
eventLoop = eventloop.NewProductionEventLoop(
|
|
entClient, worker, line.Actors, line.TempStore, sched,
|
|
bus, orderProcessor, orderProcessor.GetReplenisher(),
|
|
plcManager.GetPlc(),
|
|
momClient, c.Mom.MomConf, momBiz, dockReader,
|
|
)
|
|
eventLoop.SetOnDockChange(func() {
|
|
sseHandler.Emit(sse.EventDockDataUpdate, "{}")
|
|
})
|
|
orderProcessor.SetEventLoop(eventLoop)
|
|
|
|
go line.SignalRouter.Run(context.Background(), 1*time.Second)
|
|
recoveryCoordinator := eventloop.NewRecoveryCoordinator(entClient, bus, orderProcessor)
|
|
|
|
if err := bus.Start(context.Background()); err != nil {
|
|
slog.Error("svc: bus start failed", "error", err)
|
|
}
|
|
|
|
// 事件日志写入器
|
|
eventLogWriter := eventlog.NewEventLogWriter(entClient, 20, 1000)
|
|
eventLogWriter.Start(context.Background())
|
|
|
|
bus.SubscribeAll(func(ctx context.Context, evt eventbus.Event) error {
|
|
eventLogWriter.Write(evt)
|
|
return nil
|
|
})
|
|
|
|
// SSE 桥接器
|
|
sseBridge := sse.NewEventBridge(bus, sseHandler)
|
|
|
|
// 启动恢复
|
|
recovered, pendingOrderIDs, manualActions, err := eventloop.NewDBState(entClient).RecoverOnStartup(context.Background())
|
|
if err != nil {
|
|
slog.Error("svc: startup recovery failed", "error", err)
|
|
} else {
|
|
slog.Info("svc: startup recovery complete", "recoveredJobs", recovered, "pendingRecovery", len(pendingOrderIDs), "manualActions", len(manualActions))
|
|
eventLoop.SetRecoveryPending(pendingOrderIDs)
|
|
// 通知前端:启动时发现的不一致项需要人工确认
|
|
for _, ma := range manualActions {
|
|
orderID := 0
|
|
if ma.OrderId != nil {
|
|
orderID = *ma.OrderId
|
|
}
|
|
bus.Publish(context.Background(), eventbus.NewManualActionCreatedEvent(
|
|
ma.ID, string(ma.ActionType), orderID,
|
|
))
|
|
}
|
|
}
|
|
|
|
// 从 DB 恢复所有 Actor 槽位状态(设备 + 暂存台)
|
|
eventLoop.InitActorsFromDB(context.Background())
|
|
|
|
// 启动事件循环
|
|
go eventLoop.Run(context.Background())
|
|
|
|
// 设备数据每日上传任务(仅 MOM 启用时)
|
|
var equipUploader *processor.EquipUploadService
|
|
if momClient != nil {
|
|
equipUploader = processor.NewEquipUploadService(
|
|
dbConn, momClient, c.Mom.EquipUploadHour, c.Mom.EquipUploadDeviceTypes,
|
|
)
|
|
equipUploader.Start(context.Background())
|
|
}
|
|
|
|
slog.Info("service context: initialized",
|
|
"mock", c.Mock.Enable,
|
|
"stations", len(line.Actors),
|
|
)
|
|
|
|
svcCtx := &ServiceContext{
|
|
Config: c,
|
|
SSEHandler: sseHandler,
|
|
RedisClient: rc,
|
|
EntClient: entClient,
|
|
DB: dbConn,
|
|
PLCManager: plcManager,
|
|
RobotManager: robotManager,
|
|
RecipeLoader: recipeLoader,
|
|
OrderProcessor: orderProcessor,
|
|
EventBus: bus,
|
|
EventLogWriter: eventLogWriter,
|
|
SSEBridge: sseBridge,
|
|
EventLoop: eventLoop,
|
|
RecoveryCoordinator: recoveryCoordinator,
|
|
Mom: momClient,
|
|
EquipUploader: equipUploader,
|
|
}
|
|
|
|
return svcCtx
|
|
}
|
|
|
|
// dockStateAdapter 将 robot.Controller 适配为 DockStateReader
|
|
type dockStateAdapter struct {
|
|
ctrl *robot.Controller
|
|
}
|
|
|
|
func (a dockStateAdapter) ReadDockStates(ctx context.Context) ([]bool, error) {
|
|
if a.ctrl == nil {
|
|
return nil, fmt.Errorf("controller not available")
|
|
}
|
|
return a.ctrl.DockStates()
|
|
}
|