fix(shadow): protect observed vehicle traffic
This commit is contained in:
@@ -41,6 +41,7 @@ public final class ShadowDispatchSnapshotAssembler {
|
||||
private final Clock clock;
|
||||
private final java.util.Set<String> safeWaitSites;
|
||||
private final java.util.Set<String> safeYieldSites;
|
||||
private final ShadowObservedTrafficProjector observedTraffic = new ShadowObservedTrafficProjector();
|
||||
|
||||
public ShadowDispatchSnapshotAssembler(
|
||||
String warehouseCode,
|
||||
@@ -72,7 +73,7 @@ public final class ShadowDispatchSnapshotAssembler {
|
||||
stateLoader.apply(topology), "stateLoader result");
|
||||
return new DispatchSnapshot(tasks(batch, topology),
|
||||
vehicles(batch, topology), topology, state.getLedger(),
|
||||
state.getWaitForGraph(), state.getSnapshotVersion());
|
||||
state.getWaitForGraph(), state.getSnapshotVersion(), observedTraffic.project(batch, topology));
|
||||
}
|
||||
|
||||
private EffectiveTopology topology(List<ShadowMapFact> facts) {
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.ruoyi.shadow.kernel;
|
||||
|
||||
import com.ruoyi.shadow.facts.ShadowFactBatch;
|
||||
import com.ruoyi.shadow.facts.ShadowQueueFact;
|
||||
import com.ruoyi.shadow.facts.ShadowVehicleFact;
|
||||
import com.ruoyi.wcs.domain.topology.EffectiveTopology;
|
||||
import com.ruoyi.wcs.domain.topology.TopologyEdge;
|
||||
import com.ruoyi.wcs.domain.topology.TopologyNode;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/** Pure read-only observations; never creates or releases persisted reservations. */
|
||||
final class ShadowObservedTrafficProjector {
|
||||
private final Set<String> observedVehicleIds = new LinkedHashSet<String>();
|
||||
|
||||
synchronized Map<String, Set<String>> project(ShadowFactBatch batch, EffectiveTopology topology) {
|
||||
Map<String, Set<String>> owners = new LinkedHashMap<String, Set<String>>();
|
||||
Map<String, ShadowVehicleFact> vehicles = new LinkedHashMap<String, ShadowVehicleFact>();
|
||||
for (ShadowVehicleFact vehicle : batch.getVehicles()) {
|
||||
if (vehicles.put(vehicle.getDeviceId(), vehicle) != null) {
|
||||
throw new IllegalArgumentException("OBSERVED_VEHICLE_DUPLICATE:" + vehicle.getDeviceId());
|
||||
}
|
||||
}
|
||||
if (vehicles.isEmpty()) throw new IllegalArgumentException("OBSERVED_VEHICLES_EMPTY");
|
||||
observedVehicleIds.addAll(vehicles.keySet());
|
||||
Set<String> missing = new LinkedHashSet<String>(observedVehicleIds);
|
||||
missing.removeAll(vehicles.keySet());
|
||||
if (!missing.isEmpty()) throw new IllegalArgumentException("OBSERVED_VEHICLE_MISSING:" + missing);
|
||||
Map<String, String> coordinates = new LinkedHashMap<String, String>();
|
||||
for (String id : topology.getNodeIds()) {
|
||||
TopologyNode node = topology.node(id);
|
||||
coordinates.put(coordinate(node.getFloorCode(), node.getX(), node.getY()), id);
|
||||
}
|
||||
for (ShadowVehicleFact vehicle : batch.getVehicles()) {
|
||||
TopologyNode current = topology.node(vehicle.getCurrentSite());
|
||||
if (current == null) throw new IllegalArgumentException("OBSERVED_POSITION_UNKNOWN:" + vehicle.getDeviceId());
|
||||
if (vehicle.getCurrentFloor() != null
|
||||
&& !current.getFloorCode().equals("F" + vehicle.getCurrentFloor())) {
|
||||
throw new IllegalArgumentException("OBSERVED_FLOOR_CONFLICT:" + vehicle.getDeviceId());
|
||||
}
|
||||
add(owners, "NODE:" + current.getNodeId(), vehicle.getDeviceId());
|
||||
boolean confirmedIdle = Integer.valueOf(1).equals(vehicle.getStatus())
|
||||
&& Integer.valueOf(1).equals(vehicle.getAllowSend());
|
||||
if (!confirmedIdle) {
|
||||
if (!addPath(owners, vehicle.getPath(), vehicle.getDeviceId(), current, topology, coordinates)) {
|
||||
blockFloor(owners, current.getFloorCode(), vehicle.getDeviceId(), topology);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (ShadowQueueFact queue : batch.getQueues()) {
|
||||
if (!queue.isActive() || (!Integer.valueOf(1).equals(queue.getIssued())
|
||||
&& !Integer.valueOf(3).equals(queue.getExecutionStatus()))) continue;
|
||||
String owner = queue.getAgvId() == null ? "" : queue.getAgvId().trim();
|
||||
ShadowVehicleFact vehicle = vehicles.get(owner);
|
||||
if (vehicle == null) throw new IllegalArgumentException("OBSERVED_QUEUE_VEHICLE_UNKNOWN:" + queue.getGuid());
|
||||
if (!addPath(owners, queue.getPathInfo(), owner, topology.node(vehicle.getCurrentSite()), topology, coordinates)) {
|
||||
blockFloor(owners, topology.node(vehicle.getCurrentSite()).getFloorCode(), owner, topology);
|
||||
}
|
||||
}
|
||||
return owners;
|
||||
}
|
||||
|
||||
// path/path_info are movement-ordered, not the PLC [start,end,turns] encoding.
|
||||
// Expand straight segments through topology coordinates, including WY/TD nodes.
|
||||
private static boolean addPath(Map<String, Set<String>> owners, String path, String owner,
|
||||
TopologyNode current, EffectiveTopology topology, Map<String, String> coordinates) {
|
||||
if (path == null || path.trim().isEmpty()) return false;
|
||||
List<String> anchors = new ArrayList<String>();
|
||||
for (String token : path.split(",")) {
|
||||
String id = token.trim();
|
||||
if (id.isEmpty() || !topology.containsNode(id)) return false;
|
||||
if (!topology.node(id).getFloorCode().equals(current.getFloorCode())) {
|
||||
throw new IllegalArgumentException("OBSERVED_PATH_CROSS_FLOOR:" + owner);
|
||||
}
|
||||
anchors.add(id);
|
||||
}
|
||||
if (anchors.isEmpty()) return false;
|
||||
Set<String> resources = new LinkedHashSet<String>();
|
||||
resources.add("NODE:" + anchors.get(0));
|
||||
for (int i = 1; i < anchors.size(); i++) {
|
||||
TopologyNode from = topology.node(anchors.get(i - 1));
|
||||
TopologyNode to = topology.node(anchors.get(i));
|
||||
if (!from.getFloorCode().equals(to.getFloorCode())
|
||||
|| (from.getX() != to.getX() && from.getY() != to.getY())) return false;
|
||||
int dx = Integer.compare(to.getX(), from.getX());
|
||||
int dy = Integer.compare(to.getY(), from.getY());
|
||||
int x = from.getX(), y = from.getY();
|
||||
String previous = from.getNodeId();
|
||||
while (x != to.getX() || y != to.getY()) {
|
||||
x += dx;
|
||||
y += dy;
|
||||
String next = coordinates.get(coordinate(from.getFloorCode(), x, y));
|
||||
if (next == null || topology.edge(previous, next) == null) return false;
|
||||
resources.add("NODE:" + next);
|
||||
resources.add(TopologyEdge.canonicalResourceKey(previous, next));
|
||||
previous = next;
|
||||
}
|
||||
}
|
||||
if (!resources.contains("NODE:" + current.getNodeId())) return false;
|
||||
for (String key : resources) add(owners, key, owner);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void blockFloor(Map<String, Set<String>> owners, String floor,
|
||||
String owner, EffectiveTopology topology) {
|
||||
for (String id : topology.getNodeIds()) {
|
||||
if (topology.node(id).getFloorCode().equals(floor)) add(owners, "NODE:" + id, owner);
|
||||
}
|
||||
}
|
||||
|
||||
private static void add(Map<String, Set<String>> owners, String resource, String owner) {
|
||||
owners.computeIfAbsent(resource, key -> new LinkedHashSet<String>()).add(owner);
|
||||
}
|
||||
|
||||
private static String coordinate(String floor, int x, int y) { return floor + ':' + x + ':' + y; }
|
||||
}
|
||||
Reference in New Issue
Block a user