package processor import ( "context" "fmt" "log/slog" "bj_power_mes/constants" "bj_power_mes/ent" "bj_power_mes/ent/workorderpart" "bj_power_mes/internal/cnc" "bj_power_mes/internal/preload" "bj_power_mes/internal/processor/actor" "bj_power_mes/internal/processor/eventloop" "bj_power_mes/internal/processor/station" "bj_power_mes/internal/robot" "bj_power_mes/internal/sse" goplc "bjhardman.cn/bjhardman/goplc" ) // ProductionLine 生产产线聚合(Actor + SignalRouter + TempStoreActor) type ProductionLine struct { Actors map[int]actor.MachineActor Stations map[int]station.Station SignalRouter *actor.SignalRouter TempStore *actor.TempStoreActor } // BuildProductionLine 从 DB 设备表构建 Actor 产线。 // onDone/onInspect/onSlotFree 回调在 Actor 信号处理后触发,用于投递事件到 EventLoop。 func BuildProductionLine( ctx context.Context, entClient *ent.Client, dbState *eventloop.DBState, plc goplc.Client, robotCtrl *robot.Controller, onDone actor.OnMachineEventFunc, onInspect actor.OnInspectionEventFunc, onSlotFree actor.OnSlotFreeFunc, sseHandler *sse.Handler, ) (*ProductionLine, error) { doneSignals, ngSignals := buildSignalMaps(ctx, entClient) signalRouter := actor.NewSignalRouter(plc) machineActors := make(map[int]actor.MachineActor) equipments, err := entClient.Equipment.Query().WithEquipmentType().All(ctx) if err != nil { slog.Error("factory: query equipment failed, production line cannot start", "error", err) return nil, fmt.Errorf("query equipment: %w", err) } for _, eq := range equipments { isInsp := eq.Edges.EquipmentType.Code == "INSPECTION" || eq.Edges.EquipmentType.Code == "SAMPLING" signalCh := signalRouter.Watch(eq.ID, doneSignals[eq.ID], ngSignals[eq.ID]) cfg := actor.MachineActorConfig{ ID: eq.ID, Type: string(eq.Edges.EquipmentType.Code), Capacity: eq.SlotCount, Batch: eq.Batch, Inspection: isInsp, SignalCh: signalCh, DB: dbState, OnDone: onDone, OnInspect: onInspect, OnSlotFree: onSlotFree, } a := actor.NewMachineActor(cfg) machineActors[eq.ID] = a go a.Run(ctx) } configureActorProductTypes(ctx, entClient, machineActors) tempStoreActor := actor.NewTempStoreActor(dbState.TempSlotCapacity(ctx), dbState, actor.WithTempStoreOnChange(func() { sseHandler.Emit(sse.EventTempStationUpdate, "{}") }), ) go tempStoreActor.Run(ctx) slog.Info("build production line: done", "actors", len(machineActors)) // FANUC 通讯管理器:地址从 equipment.ipAddress 读取(格式 "IP:端口"), // 高压清洗机放料前通过 FANUC 写宏变量 #666/#888 cncMgr := cnc.NewManager(buildCncAddrMap(ctx, entClient)) return &ProductionLine{ Actors: machineActors, Stations: buildStations(equipments, robotCtrl, cncMgr, washerGroupNoFinder(entClient)), SignalRouter: signalRouter, TempStore: tempStoreActor, }, nil } // buildCncAddrMap 从 equipment 表构建 machineID → ipAddress("IP:端口")映射 func buildCncAddrMap(ctx context.Context, entClient *ent.Client) map[int]string { addrs := make(map[int]string) equips, err := entClient.Equipment.Query().All(ctx) if err != nil { slog.Warn("factory: failed to query equipment for cnc addresses", "error", err) return addrs } for _, e := range equips { if e.IpAddress != "" { addrs[e.ID] = e.IpAddress } } return addrs } // washerGroupNoFinder jobID → 清洗机组号(工单零件号 → part_no.washerGroupNo) // 链路:job → work_order_id → work_order_part.part_no_id → part_no.washer_group_no func washerGroupNoFinder(entClient *ent.Client) func(ctx context.Context, jobID int) (int, error) { return func(ctx context.Context, jobID int) (int, error) { j, err := entClient.Job.Get(ctx, jobID) if err != nil { return 0, fmt.Errorf("job %d not found: %w", jobID, err) } wop, err := entClient.WorkOrderPart.Query(). Where(workorderpart.WorkOrderIdEQ(j.WorkOrderId)). First(ctx) if err != nil { return 0, fmt.Errorf("work order %d has no part no: %w", j.WorkOrderId, err) } pn, err := entClient.PartNo.Get(ctx, wop.PartNoId) if err != nil { return 0, fmt.Errorf("part no %d not found: %w", wop.PartNoId, err) } return pn.WasherGroupNo, nil } } // buildSignalMaps 从 DB equipment 表 + preload 信号映射构建 machineID→PLC地址 func buildSignalMaps(ctx context.Context, entClient *ent.Client) (doneSignals, ngSignals map[int]string) { doneSignals = make(map[int]string) ngSignals = make(map[int]string) equips, err := entClient.Equipment.Query().All(ctx) if err != nil { slog.Warn("factory: failed to query equipment for signal maps", "error", err) return } for _, e := range equips { if e.DoneSignalName != "" { addr := preload.GetMAddress(e.DoneSignalName) if addr.Name != "" { doneSignals[e.ID] = addr.Addr.String() } else { slog.Warn("factory: done signal not found", "equipment", e.Name, "signalName", e.DoneSignalName) } } if e.NgSignalName != "" { addr := preload.GetMAddress(e.NgSignalName) if addr.Name != "" { ngSignals[e.ID] = addr.Addr.String() } else { slog.Warn("factory: ng signal not found", "equipment", e.Name, "signalName", e.NgSignalName) } } } return } // configureActorProductTypes 从 DB product_type 表配置 CNC 可接受的产品类型 func configureActorProductTypes(ctx context.Context, entClient *ent.Client, actors map[int]actor.MachineActor) { productTypes, err := entClient.ProductType.Query().All(ctx) if err != nil { slog.Error("factory: query product types for Actor config failed", "error", err) return } for _, pt := range productTypes { for _, cncID := range pt.CncMachineIds { if a, ok := actors[cncID]; ok { a.AddAllowedProductType(pt.ID) } } } } // buildStations 从设备列表构建 Station map(key=machineID)。 // TEMP_STORE 和 ROBOT 类型不创建 Station,返回 nil。 func buildStations(equipments []*ent.Equipment, robotCtrl *robot.Controller, cncMgr *cnc.Manager, groupNoFinder func(ctx context.Context, jobID int) (int, error)) map[int]station.Station { stations := make(map[int]station.Station) for _, eq := range equipments { typeCode := constants.EquipmentTypeCode(eq.Edges.EquipmentType.Code) if typeCode == constants.EquipmentTypeCode_TempStore || typeCode == constants.EquipmentTypeCode_Robot { continue } if robotCtrl == nil { continue } var st station.Station if typeCode == constants.EquipmentTypeCode_WashingMachine { // 高压清洗机:放料前先写 FANUC 宏变量 #666/#888(组号来自工单零件号),再发 PLC 命令 st = station.NewWasherStation(robotCtrl, eq.ID, &station.FanucOption{ WriteMacro: func(ctx context.Context, macroNo int, value float64) error { return cncMgr.WriteMacro(ctx, eq.ID, macroNo, value) }, GroupNo: groupNoFinder, }) } else { st = station.NewByType(typeCode, eq.ID, robotCtrl) } if st != nil { stations[eq.ID] = st } } slog.Info("factory: built stations", "count", len(stations)) return stations }