211 lines
7.0 KiB
Go
211 lines
7.0 KiB
Go
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
|
||
}
|