package eventloop import ( "context" "fmt" "log/slog" "strconv" "sync/atomic" "time" "bj_power_mes/constants" "bj_power_mes/ent" "bj_power_mes/ent/equipmentslot" "bj_power_mes/internal/eventbus" "bj_power_mes/internal/processor/actor" "bj_power_mes/internal/processor/scheduler" momlib "bjhardman.cn/bjhardman/mom" ) // PLCReader reads PLC boolean signals type PLCReader interface { ReadBool(address string) (bool, error) } // DockStateReader 从 PLC 读取接驳台在位状态 type DockStateReader interface { ReadDockStates(ctx context.Context) ([]bool, error) } // MomBizConfig MOM 业务相关配置(与 SDK 的 MomConf 解耦,避免循环依赖 internal/config) // // 仅放 EventLoop 自己直接使用的字段。设备上传相关参数由 EquipUploadService 单独持有。 type MomBizConfig struct { // InstoreType 入库类型 1:立库 2:平库 InstoreType int } // ProductionEventLoop 产线事件循环:唯一生产状态写路径,串行处理所有消息 type ProductionEventLoop struct { db *DBState entClient *ent.Client msgCh chan EventLoopMessage worker HardwareWorker // 运行时快照:jobID → RuntimeSnapshot jobRuntimes map[int]*RuntimeSnapshot // 飞行锁:防止同一 job 的 Worker 并发 dispatch dispatching map[int]bool // 工人忙标志:确保同一时刻只有一个 Worker 在执行 workerBusy atomic.Bool // 重置进行中标志:人工重置暂存台期间置位,阻止 trySchedule 发起新补料 dispatch, // 避免 reset 直接写 DB 与补料搬运并发。仅挡"新发起",在途 worker 由 BeginReset 拒绝。 resetting atomic.Bool // 补料器(trySchedule 末尾检查) replenisher Replenisher // 调度器 sched *scheduler.Scheduler machineActors map[int]actor.MachineActor tempStoreActor *actor.TempStoreActor // 接驳台变更回调(通知前端刷新) onDockChange func() // Job 操作 jobOps OrderProcessorInterface // 事件总线(发布 job 状态变更,供 EventLogWriter 消费) eventBus interface { Publish(context.Context, eventbus.Event) error } // PLC 信号读取(nil 表示不可用) plc PLCReader // MOM momClient *momlib.Mom momConf momlib.MomConf momBiz MomBizConfig dockReader DockStateReader lastDockStates []bool // 抽检调度标志:同一调度周期只抽一个工件 samplingDispatched bool recoveryActive atomic.Bool recoveryOrderIDs []int stopCh chan struct{} } // RuntimeSnapshot 工件运行时快照,含调度所需的 recipe 步骤信息 type RuntimeSnapshot struct { JobID int WorkpieceNo string WorkOrderID int ProductTypeID int RecipeID int Status string PositionType string PositionRefID string StepID string // 当前步骤业务ID(OP10-OP120) StepIndex int // 当前步骤序号(从 recipe_step 加载,供调度器排序) TempSlotNo int DockNo int // 接驳台号(Dock 卸料用) DockSlotNo int // recipe 步骤信息(从 DB 加载,供调度器使用) StepType constants.StepType ResourceType string ToolType string StepName string Context map[string]any } func NewProductionEventLoop( entClient *ent.Client, worker HardwareWorker, machineActors map[int]actor.MachineActor, tempStoreActor *actor.TempStoreActor, sched *scheduler.Scheduler, bus interface { Publish(context.Context, eventbus.Event) error }, jobOps OrderProcessorInterface, replenisher Replenisher, plc PLCReader, momClient *momlib.Mom, momConf momlib.MomConf, momBiz MomBizConfig, dockReader DockStateReader, ) *ProductionEventLoop { if machineActors == nil { machineActors = make(map[int]actor.MachineActor) } return &ProductionEventLoop{ db: NewDBState(entClient), entClient: entClient, worker: worker, machineActors: machineActors, tempStoreActor: tempStoreActor, sched: sched, eventBus: bus, jobOps: jobOps, replenisher: replenisher, plc: plc, momClient: momClient, momConf: momConf, momBiz: momBiz, dockReader: dockReader, lastDockStates: make([]bool, 0), msgCh: make(chan EventLoopMessage, 256), jobRuntimes: make(map[int]*RuntimeSnapshot), dispatching: make(map[int]bool), stopCh: make(chan struct{}), } } // SetOnDockChange 设置接驳台变更回调(SSE 通知前端刷新) func (l *ProductionEventLoop) SetOnDockChange(fn func()) { l.onDockChange = fn } func (l *ProductionEventLoop) notifyDockChange() { if l.onDockChange != nil { l.onDockChange() } } // ReleaseTempSlots 释放暂存台 Actor 内存中指定 job 占用的槽位。 // 用于人工重置暂存台等已直接写 DB 清空 job.temp_slot_no 的场景: // DB 已清空但 Actor 内存 slots 仍持有旧 jobID,不释放会让后续 AllocateSlot // 跳过这些槽位、最终假性判满(ErrTempSlotFull),补料停摆直到进程重启。 // 同步调用 Actor.ReleaseSlot,幂等(job 不在槽位中仅记警告;DB temp_slot_no 重复清空无害)。 func (l *ProductionEventLoop) ReleaseTempSlots(jobIDs []int) { if l.tempStoreActor == nil || len(jobIDs) == 0 { return } ctx := context.Background() for _, jobID := range jobIDs { l.tempStoreActor.ReleaseSlot(ctx, jobID) } } // BeginReset 尝试进入重置态。返回是否可进入及不可进入的原因。 // 在 reset 事务前调用:若有在途 worker/补料则拒绝(避免与正在执行的搬运并发), // 否则置 resetting 阻止后续 trySchedule 发起新补料 dispatch。必须配对调用 EndReset。 func (l *ProductionEventLoop) BeginReset() (bool, string) { if l.workerBusy.Load() { return false, "产线正在执行搬运,请稍后重试" } if l.isReplenishing() { return false, "补料进行中,请稍后重试" } l.resetting.Store(true) return true, "" } // EndReset 退出重置态,恢复补料调度。 func (l *ProductionEventLoop) EndReset() { l.resetting.Store(false) } // IsResetting 返回是否处于重置态(供调度桥接判断是否跳过补料)。 func (l *ProductionEventLoop) IsResetting() bool { return l.resetting.Load() } // SendScheduleTick 投递立即调度事件(非阻塞)。 // 用非阻塞发送避免在 event loop goroutine 内调用时死锁自己; // channel 满意味着已有待处理的调度事件,丢弃不造成功能缺失。 func (l *ProductionEventLoop) SendScheduleTick() { select { case l.msgCh <- EventLoopMessage{ ID: fmt.Sprintf("sched-tick-%d", time.Now().UnixNano()), Type: EvtScheduleTick, }: default: slog.Warn("event loop: schedule tick dropped, channel full") } } // Send 向事件循环投递消息(channel 满时阻塞) func (l *ProductionEventLoop) Send(msg EventLoopMessage) { l.msgCh <- msg } // SendSync 投递消息并等待结果 func (l *ProductionEventLoop) SendSync(msg EventLoopMessage, timeout time.Duration) (*MessageResult, error) { replyCh := make(chan MessageResult, 1) msg.Reply = replyCh l.msgCh <- msg select { case result := <-replyCh: return &result, nil case <-time.After(timeout): return nil, fmt.Errorf("event loop timeout") } } // InitActorsFromDB 启动时从 DB 恢复所有 Actor 槽位状态(设备 + 暂存台)。 func (l *ProductionEventLoop) InitActorsFromDB(ctx context.Context) { if l.entClient == nil { return } // 设备槽位:从 equipment_slot 表恢复 slots, err := l.entClient.EquipmentSlot.Query(). Where(equipmentslot.StatusIn(constants.SlotStatus_Occupied, constants.SlotStatus_Done)). All(ctx) if err != nil { slog.Error("event loop: init equipment slots from db failed", "error", err) } else { for _, s := range slots { if a, ok := l.machineActors[*s.EquipmentId]; ok { jobID := 0 if s.CurrentJobId != nil { jobID = *s.CurrentJobId } ts := time.Now() if s.OccupiedAt != nil { ts = *s.OccupiedAt } a.RestoreSlot(s.SlotNo, string(s.Status), jobID, ts) } } slog.Info("event loop: equipment slots initialized", "count", len(slots)) } // 暂存台槽位:从 job 表恢复 jobs, err := l.db.GetActiveJobs(ctx) if err != nil { slog.Error("event loop: init temp slots from db failed", "error", err) return } for _, j := range jobs { if j.TempSlotNo != nil && *j.TempSlotNo > 0 && l.tempStoreActor != nil { l.tempStoreActor.RestoreSlot(*j.TempSlotNo, j.ID) } } slog.Info("event loop: temp store slots initialized", "jobs", len(jobs)) } // Run 启动事件循环(阻塞) func (l *ProductionEventLoop) Run(ctx context.Context) { slog.Info("event loop: started") schedTimer := time.NewTimer(60 * time.Second) defer schedTimer.Stop() for { select { case msg := <-l.msgCh: l.handleMessage(ctx, msg) // Worker 完成后重置调度定时器(worker 忙时 tick 不触发调度) if !l.workerBusy.Load() { schedTimer.Reset(60 * time.Second) } case <-schedTimer.C: if !l.workerBusy.Load() { l.trySchedule(ctx) } l.tryPollMomOrders(ctx) schedTimer.Reset(60 * time.Second) case <-l.stopCh: slog.Info("event loop: stopped") return case <-ctx.Done(): slog.Info("event loop: context done") return } } } // Stop 停止事件循环 func (l *ProductionEventLoop) Stop() { close(l.stopCh) } // handleMessage 消息分发 func (l *ProductionEventLoop) handleMessage(ctx context.Context, msg EventLoopMessage) { slog.Debug("event loop: handling message", "type", msg.Type, "id", msg.ID) switch msg.Type { // Command case CmdStartOrder: l.handleStartOrder(ctx, msg) case CmdPauseOrder: l.handlePauseOrder(ctx, msg) case CmdResumeOrder: l.handleResumeOrder(ctx, msg) case CmdCancelOrder: l.handleCancelOrder(ctx, msg) case CmdStopOrder: l.handleStopOrder(ctx, msg) case CmdSuspendJob: l.handleSuspendJob(ctx, msg) case CmdResumeJob: l.handleResumeJob(ctx, msg) case CmdReworkJob: l.handleReworkJob(ctx, msg) case CmdRestoreOrder: l.handleRestoreOrder(ctx, msg) // ExternalEvent case EvtMachineDone: l.handleMachineDone(ctx, msg) case EvtStepTimeout: l.handleStepTimeout(ctx, msg) case EvtScheduleTick: l.trySchedule(ctx) case EvtAgvRequestEntry: l.handleAgvRequestEntry(ctx, msg) // WorkerResult case ResRobotActionSucceeded, ResRobotActionFailed, ResToolActionSucceeded, ResToolActionFailed, ResMachineStartSucceeded, ResMachineStartFailed: l.handleWorkerResult(ctx, msg) } } func (l *ProductionEventLoop) SetRecoveryPending(orderIDs []int) { l.recoveryOrderIDs = orderIDs l.recoveryActive.Store(len(orderIDs) > 0) } func (l *ProductionEventLoop) SyncMachineSlotStatus(jobID int, posType string) { for _, a := range l.machineActors { if slot, found := a.FindSlotByJobID(jobID); found { switch posType { case "ON_EQUIPMENT_DONE": a.RestoreSlot(slot, string(constants.SlotStatus_Done), jobID, time.Now()) case "ON_EQUIPMENT_PROCESSING": a.RestoreSlot(slot, string(constants.SlotStatus_Occupied), jobID, time.Now()) default: a.ReleaseSlot(slot) } } } } func (l *ProductionEventLoop) IsRecoveryActive() bool { return l.recoveryActive.Load() } func (l *ProductionEventLoop) OccupyTempSlot(slotNo, jobID int) { if l.tempStoreActor != nil { l.tempStoreActor.RestoreSlot(slotNo, jobID) } } func (l *ProductionEventLoop) CompleteRecovery(ctx context.Context) error { for _, id := range l.recoveryOrderIDs { _, err := l.entClient.WorkOrder.UpdateOneID(id). SetStatus(constants.WorkOrderStatus_Paused). Save(ctx) if err != nil { return fmt.Errorf("complete recovery: order %d: %w", id, err) } } l.recoveryOrderIDs = nil l.recoveryActive.Store(false) slog.Info("recovery: completed, orders set to PAUSED") l.SendScheduleTick() return nil } func (l *ProductionEventLoop) reply(msg EventLoopMessage, success bool, errMsg string, data map[string]any) { if msg.Reply != nil { msg.Reply <- MessageResult{Success: success, Error: errMsg, Data: data} } } // handleMachineDone 设备完成信号处理 // handleMachineDone 处理设备完成信号(Actor 已管理槽位状态 + 写 DB,EventLoop 只需推进步骤) func (l *ProductionEventLoop) handleMachineDone(ctx context.Context, msg EventLoopMessage) { machineID := intFromPayload(msg.Payload, "machineId") if machineID == 0 { return } if l.entClient == nil { l.trySchedule(ctx) return } // 检测结果:Actor 已标记槽位或直接回调,EventLoop 根据 pass/fail 处理 if inspectPass, hasInspect := msg.Payload["inspectionPass"].(bool); hasInspect { l.handleInspectionDone(ctx, machineID, inspectPass) l.trySchedule(ctx) return } // 普通设备完成:Actor 已标记槽位 Done + 写 DB,EventLoop 推进步骤 jobID := intFromPayload(msg.Payload, "jobId") if jobID == 0 { return } // CNC 设备 OPERATION 步骤:记录加工完成时间 if l.db.IsCNCEquipment(ctx, machineID) && l.isOperationStep(jobID) { if err := l.db.SetCncCompletedAt(ctx, jobID); err != nil { slog.Error("event loop: set cnc completed at failed", "jobId", jobID, "error", err) } } // 所有 OPERATION 步骤发布 MACHINE_DONE(EventLogWriter 会更新对应 MACHINE_START 的 endedAt) if l.isOperationStep(jobID) { machineDonePayload := map[string]any{ "jobId": jobID, "equipmentId": machineID, "equipmentName": equipmentName(machineID), "endedAt": time.Now(), } // 补充工件编号和加工时长 doneJob, _ := l.entClient.Job.Get(ctx, jobID) if doneJob != nil { machineDonePayload["workpieceNo"] = doneJob.WorkpieceNo if !doneJob.CncStartedAt.IsZero() { machineDonePayload["startedAt"] = doneJob.CncStartedAt machineDonePayload["duration"] = fmt.Sprintf("%.0f秒", time.Since(doneJob.CncStartedAt).Seconds()) } } l.publishEvent(ctx, eventbus.EventMachineDone, "job", strconv.Itoa(jobID), machineDonePayload) } job, _ := l.entClient.Job.Get(ctx, jobID) if job != nil && job.Status == constants.JobStatus_Processing { if l.isOperationStep(jobID) { l.db.SetJobWaitingUnload(ctx, jobID) } else { finished, _ := l.db.AdvanceStep(ctx, jobID, nil, "", "") if finished { l.checkPalletCompletion(ctx, jobID) l.checkOrderCompletion(ctx, jobID) } else { l.entClient.Job.UpdateOneID(jobID). SetStatus(constants.JobStatus_WaitingUnload). Save(ctx) } } } if l.jobOps != nil { l.jobOps.WakeJob(jobID) } l.trySchedule(ctx) } // handleInspectionDone 检测结果处理(由 handleMachineDone 统一调用,slot 已设为 Done) func (l *ProductionEventLoop) handleInspectionDone(ctx context.Context, machineID int, pass bool) { slot, _ := l.entClient.EquipmentSlot.Query(). Where(equipmentslot.EquipmentIdEQ(machineID), equipmentslot.StatusEQ(constants.SlotStatus_Done)). First(ctx) if slot == nil { slog.Warn("event loop: inspection done, no done slot found", "machineId", machineID) return } jobID := slotJobID(slot) if jobID == 0 { slog.Warn("event loop: inspection done, no job on slot", "machineId", machineID) return } // 设备类型决定检测结果写入哪个字段(INSPECTION/SAMPLING) var machineType string if a, ok := l.machineActors[machineID]; ok { machineType = a.Type() } // 发布检测结果事件 inspPayload := map[string]any{ "jobId": jobID, "equipmentId": machineID, "equipmentName": equipmentName(machineID), "pass": pass, "endedAt": time.Now(), } inspJob, _ := l.entClient.Job.Get(ctx, jobID) if inspJob != nil { inspPayload["workpieceNo"] = inspJob.WorkpieceNo } l.publishEvent(ctx, eventbus.EventInspectionResult, "job", strconv.Itoa(jobID), inspPayload) if pass { if l.isOperationStep(jobID) { jb, err := l.entClient.Job.Get(ctx, jobID) if err != nil { slog.Error("event loop: inspection done, get job failed", "jobId", jobID, "error", err) return } if err := l.db.CompleteStep(ctx, jobID, jb.CurrentStepId, map[string]any{"inspectionPass": true}); err != nil { slog.Error("event loop: inspection done, complete step failed", "jobId", jobID, "error", err) return } if err := l.db.SetJobWaitingUnload(ctx, jobID); err != nil { slog.Error("event loop: inspection done, set waiting unload failed", "jobId", jobID, "error", err) return } } else { finished, err := l.db.AdvanceStep(ctx, jobID, map[string]any{"inspectionPass": true}, "", "") if err != nil { slog.Error("event loop: inspection done, advance step failed", "jobId", jobID, "error", err) } else if finished { l.checkPalletCompletion(ctx, jobID) l.checkOrderCompletion(ctx, jobID) } } // 检测合格:写入独立结果字段(与 context.inspectionPass 双写) if machineType != "" { if err := l.db.SetQCResult(ctx, jobID, machineType, true); err != nil { slog.Error("event loop: set qc result(pass) failed", "jobId", jobID, "error", err) } } } else { job, err := l.entClient.Job.Get(ctx, jobID) if err != nil { slog.Error("event loop: inspection done, job not found", "jobId", jobID, "error", err) return } // 状态守卫:如果 job 已不在 PROCESSING(如已被 SignalDone 通过),跳过报废 if job.Status != constants.JobStatus_Processing { slog.Warn("event loop: inspection fail ignored, job already advanced", "jobId", jobID, "status", job.Status) return } // 记录检测结果到 job context(留作审计追溯),再报废工件 l.db.CompleteStep(ctx, jobID, job.CurrentStepId, map[string]any{"inspectionPass": false}) // 同步写入独立结果字段(FAIL),作为合格率统计一等数据源 if machineType != "" { if err := l.db.SetQCResult(ctx, jobID, machineType, false); err != nil { slog.Error("event loop: set qc result(fail) failed", "jobId", jobID, "error", err) } } _ = l.db.RaiseAlarm(ctx, "INSPECTION_FAIL", fmt.Sprintf("工件 %s 检测不合格", job.WorkpieceNo), constants.AlarmLevel_WARN, machineID, jobID, "event-loop") _ = l.db.FinishJob(ctx, jobID, job.WorkOrderId, constants.JobStatus_Scrapped) l.checkPalletCompletion(ctx, jobID) l.checkOrderCompletion(ctx, jobID) l.publishProductionEvent(ctx, eventbus.EventJobScrapped, jobID, job.WorkpieceNo, 0, "", 0, nil) _ = l.db.SetEquipmentSlot(ctx, machineID, slot.SlotNo, constants.SlotStatus_Done, constants.SlotStatus_Empty, 0) // 同步释放 actor 内存槽位(NG 不走到 UNLOAD_COMPLETE 路径) if a, ok := l.machineActors[machineID]; ok { a.ReleaseSlot(slot.SlotNo) } } } // isOperationStep 判断 job 当前步骤是否为 OPERATION 类型。 // 优先查内存缓存;缓存缺失时 fallback 到 DB 查询,防止 handleMachineDone 等场景下 // 因 RuntimeSnapshot 尚未刷新而错误地走了 AdvanceStep 分支。 func (l *ProductionEventLoop) isOperationStep(jobID int) bool { if snap := l.jobRuntimes[jobID]; snap != nil { return snap.StepType == constants.StepType_Operation } // 缓存缺失 → 查 DB if l.db != nil { jb, err := l.entClient.Job.Get(context.Background(), jobID) if err != nil { return false } _, stepType, _, _, _, err := l.db.GetRecipeStepByID(context.Background(), jb.RecipeId, jb.CurrentStepId) if err != nil { return false } return stepType == constants.StepType_Operation } return false } // slotJobID 从槽位安全获取 jobID func slotJobID(s *ent.EquipmentSlot) int { if s.CurrentJobId != nil { return *s.CurrentJobId } return 0 } // intFromPayload 从 payload 中提取 int 值 func intFromPayload(p map[string]any, key string) int { switch v := p[key].(type) { case int: return v case float64: return int(v) } return 0 } // stringFromPayload 从 payload 中提取 string 值 func stringFromPayload(p map[string]any, key string) string { if v, ok := p[key].(string); ok { return v } return "" }