package com.ruoyi.shadow.kernel; import com.ruoyi.shadow.facts.ShadowFactBatch; import com.ruoyi.shadow.facts.ShadowMapFact; import com.ruoyi.shadow.facts.ShadowQueueFact; import com.ruoyi.shadow.facts.ShadowQueueEndpoints; import com.ruoyi.shadow.facts.ShadowVehicleFact; import com.ruoyi.wcs.application.dispatch.DispatchSnapshot; import com.ruoyi.wcs.domain.dispatch.DispatchTask; import com.ruoyi.wcs.domain.equipment.DeviceCapability; import com.ruoyi.wcs.domain.equipment.DeviceDescriptor; import com.ruoyi.wcs.domain.equipment.DeviceType; import com.ruoyi.wcs.domain.equipment.VehicleSnapshot; import com.ruoyi.wcs.domain.topology.EffectiveTopology; import com.ruoyi.wcs.domain.topology.ResourceDefinition; import com.ruoyi.wcs.domain.topology.TopologyCompiler; import com.ruoyi.wcs.domain.topology.TopologyDefinition; import com.ruoyi.wcs.domain.topology.TopologyEdge; import com.ruoyi.wcs.domain.topology.TopologyNode; import java.time.Clock; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.function.Function; public final class ShadowDispatchSnapshotAssembler { private static final String ACTIVE_QUEUE = "shadow_ro.v_wcs_srgv_queue"; private final String warehouseCode; private final long topologyVersion; private final Function stateLoader; private final Clock clock; private final java.util.Set safeWaitSites; private final java.util.Set safeYieldSites; private final ShadowObservedTrafficProjector observedTraffic = new ShadowObservedTrafficProjector(); public ShadowDispatchSnapshotAssembler( String warehouseCode, long topologyVersion, Function stateLoader, Clock clock) { this(warehouseCode, topologyVersion, stateLoader, clock, Collections.emptySet(), Collections.emptySet()); } public ShadowDispatchSnapshotAssembler(String warehouseCode, long topologyVersion, Function stateLoader, Clock clock, java.util.Set safeWaitSites, java.util.Set safeYieldSites) { this.warehouseCode = required(warehouseCode, "warehouseCode"); if (topologyVersion <= 0L) { throw new IllegalArgumentException( "topologyVersion must be positive"); } this.topologyVersion = topologyVersion; this.stateLoader = Objects.requireNonNull(stateLoader, "stateLoader"); this.clock = Objects.requireNonNull(clock, "clock"); this.safeWaitSites = safeSites(safeWaitSites); this.safeYieldSites = safeSites(safeYieldSites); } public DispatchSnapshot assemble(ShadowFactBatch batch) { Objects.requireNonNull(batch, "batch"); EffectiveTopology topology = topology(batch.getMaps()); ShadowTrafficState state = Objects.requireNonNull( stateLoader.apply(topology), "stateLoader result"); return new DispatchSnapshot(tasks(batch, topology), vehicles(batch, topology), topology, state.getLedger(), state.getWaitForGraph(), state.getSnapshotVersion(), observedTraffic.project(batch, topology)); } private EffectiveTopology topology(List facts) { List nodes = new ArrayList(); Map coordinates = new LinkedHashMap(); for (ShadowMapFact fact : facts) { if (fact.getValue() != 1 && fact.getValue() != 2) { continue; } nodes.add(new TopologyNode(fact.getSiteId(), floor(fact.getZ()), fact.getX(), fact.getY(), 1, safeWaitSites.contains(fact.getSiteId()), safeYieldSites.contains(fact.getSiteId()))); String coordinate = coordinate(fact.getX(), fact.getY(), fact.getZ()); if (coordinates.put(coordinate, fact) != null) { throw new IllegalArgumentException( "duplicate passable map coordinate: " + coordinate); } } if (nodes.isEmpty()) { throw new IllegalArgumentException("map has no passable nodes"); } List edges = new ArrayList(); for (ShadowMapFact fact : coordinates.values()) { addEdge(edges, fact, coordinates.get(coordinate( fact.getX() + 1, fact.getY(), fact.getZ()))); addEdge(edges, fact, coordinates.get(coordinate( fact.getX(), fact.getY() + 1, fact.getZ()))); } TopologyDefinition definition = new TopologyDefinition(warehouseCode, topologyVersion, true, nodes, edges, Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); EffectiveTopology topology = new TopologyCompiler().compile(definition); java.util.Set configured = new java.util.LinkedHashSet(safeWaitSites); configured.addAll(safeYieldSites); for (String id : configured) { if (!topology.containsNode(id) || topology.neighbors(id).size() < 2) { throw new IllegalArgumentException("INVALID_CONFIGURED_SAFE_SITE:" + id); } } return topology; } public static java.util.Set safeSites(java.util.Set values) { java.util.Set normalized = new java.util.TreeSet(); for (String value : Objects.requireNonNull(values, "safeSites")) { String id = required(value, "safeSite"); if (!id.matches("[0-9]{1,6}") || Integer.parseInt(id) == 0) throw new IllegalArgumentException("INVALID_CONFIGURED_SAFE_SITE:" + id); normalized.add(id); } return Collections.unmodifiableSet(normalized); } private List tasks(ShadowFactBatch batch, EffectiveTopology topology) { List tasks = new ArrayList(); Instant now = clock.instant(); for (ShadowQueueFact fact : batch.getQueues()) { if (com.ruoyi.shadow.facts.ShadowQueueDispatchEligibility.observationReason(fact, batch) != null) { continue; } String source = required(ShadowQueueEndpoints.dispatchStart(fact, batch), "startSite"); String destination = required(ShadowQueueEndpoints.end(fact, batch), "endSite"); if (!topology.containsNode(source) || !topology.containsNode(destination)) { throw new IllegalArgumentException( "active queue endpoint is outside passable topology: " + fact.getGuid()); } Map command = commandParameters(fact); tasks.add(new DispatchTask(required(fact.getTaskNo(), "taskNo"), fact.getGuid(), source, destination, priority(fact), waitingAge(fact.getCreatedAt(), now), 31, EnumSet.of(DeviceCapability.FOUR_WAY_MOVE), command)); } return tasks; } private Map commandParameters(ShadowQueueFact fact) { Map values = new LinkedHashMap(); String parentTaskId = required(fact.getTaskNo(), "taskNo"); values.put("parentTaskId", parentTaskId); values.put("phase", phase(required(fact.getTaskType(), "taskType"))); values.put("routeVersion", Long.toString(topologyVersion)); String boundVehicle = optional(fact.getAgvId()); if (boundVehicle != null && !"0".equals(boundVehicle)) { values.put("shadowBoundVehicleId", boundVehicle); } values.put("taskNo", positiveNumber(parentTaskId, "taskNo")); short taskType = parsedShort(required(fact.getDefault02(), "default02"), "default02"); values.put("taskType", Short.toString(taskType)); String stage = optional(fact.getDefault08()); if (stage != null) { if (!"1".equals(stage) && !"2".equals(stage)) { throw new IllegalArgumentException("default08 must be 1 or 2"); } values.put("stage", stage); } String target = optional(fact.getDefault07()); if (taskType == 8) { if (target == null) { throw new IllegalArgumentException( "default07 is required for cross-floor task"); } values.put("crossFloorTarget", positiveNumber(target, "default07")); } if (stage != null && taskType == 8) { throw new IllegalArgumentException( "cross-floor and two-stage parameters cannot be combined"); } return values; } private List vehicles(ShadowFactBatch batch, EffectiveTopology topology) { List result = new ArrayList(); for (ShadowVehicleFact fact : batch.getVehicles()) { String location = required(fact.getCurrentSite(), "currentSite"); if (!topology.containsNode(location)) { throw new IllegalArgumentException( "vehicle is outside passable topology: " + fact.getDeviceId()); } String floor = fact.getCurrentFloor() == null ? topology.node(location).getFloorCode() : floor(fact.getCurrentFloor().intValue()); DeviceDescriptor descriptor = new DeviceDescriptor( fact.getDeviceId(), DeviceType.FOUR_WAY_SHUTTLE, floor, EnumSet.of(DeviceCapability.FOUR_WAY_MOVE, DeviceCapability.LOAD_HANDLING, DeviceCapability.LIFT_TRANSFER)); // Read-only projection of the legacy PLC gate; agvStatus is not refreshed by PLC polling. boolean connectedAndSendable = Integer.valueOf(5).equals(fact.getWorkMode()) && Integer.valueOf(1).equals(fact.getAllowSend()); boolean healthy = zeroOrMissing(fact.getAlarm()) && zeroOrMissing(fact.getErrorCode()); boolean idle = Integer.valueOf(1).equals(fact.getStatus()); result.add(new VehicleSnapshot(descriptor, connectedAndSendable ? VehicleSnapshot.ConnectionState.ONLINE : VehicleSnapshot.ConnectionState.OFFLINE, Integer.valueOf(5).equals(fact.getWorkMode()) ? VehicleSnapshot.OperatingMode.AUTO : VehicleSnapshot.OperatingMode.MANUAL, healthy ? VehicleSnapshot.HealthState.NORMAL : VehicleSnapshot.HealthState.FAULT, idle ? VehicleSnapshot.OccupationState.IDLE : VehicleSnapshot.OccupationState.BUSY, location, Integer.valueOf(1).equals(fact.getCargoFlag()) ? "LOAD_PRESENT" : null, fact.getBattery().intValue(), positive(fact.getTaskNo()) ? Integer.toString(fact.getTaskNo()) : null, floor, fact.getCapturedAt())); } return result; } private static void addEdge(List edges, ShadowMapFact first, ShadowMapFact second) { if (second != null) { edges.add(new TopologyEdge(first.getSiteId(), second.getSiteId(), 1, 1)); } } private static int priority(ShadowQueueFact fact) { if (fact.getPriority() == null) { return Integer.MIN_VALUE; } return (int) Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, fact.getPriority().longValue())); } private static long waitingAge(Instant createdAt, Instant now) { if (createdAt == null || createdAt.isAfter(now)) { throw new IllegalArgumentException( "active queue createdAt must not be missing or future"); } return Duration.between(createdAt, now).getSeconds(); } private static String phase(String taskType) { if ("1".equals(taskType)) { return "EMPTY_TO_PICKUP"; } if ("2".equals(taskType)) { return "LOADED_TO_PLACE"; } return "LEGACY_PHASE_" + taskType; } private static String floor(int value) { return "F" + value; } private static String coordinate(int x, int y, int z) { return z + ":" + x + ":" + y; } private static boolean zeroOrMissing(Integer value) { return value == null || value.intValue() == 0; } private static boolean positive(Integer value) { return value != null && value.intValue() > 0; } private static String positiveNumber(String value, String field) { long parsed; try { parsed = Long.parseLong(required(value, field)); } catch (NumberFormatException failure) { throw new IllegalArgumentException(field + " must be numeric", failure); } if (parsed <= 0L) { throw new IllegalArgumentException(field + " must be positive"); } return Long.toString(parsed); } private static short parsedShort(String value, String field) { try { return Short.parseShort(value); } catch (NumberFormatException failure) { throw new IllegalArgumentException(field + " must be int16", failure); } } private static String optional(String value) { return value == null || value.trim().isEmpty() ? null : value.trim(); } private static String required(String value, String field) { String normalized = optional(value); if (normalized == null) { throw new IllegalArgumentException(field + " must not be blank"); } return normalized; } }