feat(ft546): run shared dispatch kernel in shadow
This commit is contained in:
@@ -0,0 +1,288 @@
|
||||
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.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<EffectiveTopology, ShadowTrafficState> stateLoader;
|
||||
private final Clock clock;
|
||||
|
||||
public ShadowDispatchSnapshotAssembler(
|
||||
String warehouseCode,
|
||||
long topologyVersion,
|
||||
Function<EffectiveTopology, ShadowTrafficState> stateLoader,
|
||||
Clock clock) {
|
||||
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");
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
private EffectiveTopology topology(List<ShadowMapFact> facts) {
|
||||
List<TopologyNode> nodes = new ArrayList<TopologyNode>();
|
||||
Map<String, ShadowMapFact> coordinates =
|
||||
new LinkedHashMap<String, ShadowMapFact>();
|
||||
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, false, false));
|
||||
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<TopologyEdge> edges = new ArrayList<TopologyEdge>();
|
||||
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.<ResourceDefinition>emptyList(),
|
||||
Collections.<String>emptyList(),
|
||||
Collections.<String>emptyList());
|
||||
return new TopologyCompiler().compile(definition);
|
||||
}
|
||||
|
||||
private List<DispatchTask> tasks(ShadowFactBatch batch,
|
||||
EffectiveTopology topology) {
|
||||
List<DispatchTask> tasks = new ArrayList<DispatchTask>();
|
||||
Instant now = clock.instant();
|
||||
for (ShadowQueueFact fact : batch.getQueues()) {
|
||||
if (!ACTIVE_QUEUE.equals(fact.getTableName())) {
|
||||
continue;
|
||||
}
|
||||
String source = required(fact.getStartSite(), "startSite");
|
||||
String destination = required(fact.getEndSite(), "endSite");
|
||||
if (!topology.containsNode(source)
|
||||
|| !topology.containsNode(destination)) {
|
||||
throw new IllegalArgumentException(
|
||||
"active queue endpoint is outside passable topology: "
|
||||
+ fact.getGuid());
|
||||
}
|
||||
Map<String, String> 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<String, String> commandParameters(ShadowQueueFact fact) {
|
||||
Map<String, String> values = new LinkedHashMap<String, String>();
|
||||
String parentTaskId = required(fact.getTaskNo(), "taskNo");
|
||||
values.put("parentTaskId", parentTaskId);
|
||||
values.put("phase", phase(required(fact.getTaskType(), "taskType")));
|
||||
values.put("routeVersion", Long.toString(topologyVersion));
|
||||
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<VehicleSnapshot> vehicles(ShadowFactBatch batch,
|
||||
EffectiveTopology topology) {
|
||||
List<VehicleSnapshot> result = new ArrayList<VehicleSnapshot>();
|
||||
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));
|
||||
boolean connectedAndSendable = "1".equals(optional(
|
||||
fact.getAgvStatus()))
|
||||
&& 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<TopologyEdge> 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user