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

641 lines
20 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 ""
}