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
|
|||
|
|
}
|