Files
bj_power/bj_power_mes/internal/processor/eventloop/loop.go
T

641 lines
20 KiB
Go
Raw Normal View History

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 // 当前步骤业务IDOP10-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 已管理槽位状态 + 写 DBEventLoop 只需推进步骤)
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 + 写 DBEventLoop 推进步骤
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_DONEEventLogWriter 会更新对应 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 ""
}