diff --git a/javaTest/README.md b/javaTest/README.md new file mode 100644 index 0000000..46e6d70 --- /dev/null +++ b/javaTest/README.md @@ -0,0 +1,48 @@ +# FT546 WCS 中心调度与交通管制 Demo + +这是一个独立的 Java 8 Maven Demo,用于模拟 FT546 WCS 的 LEGACY 中心调度和交通管制行为。 + +## 安全边界 + +- 仅使用内存车辆、任务、队列和地图。 +- 不启动 Spring,不连接 SQL Server、PLC 或 WMS。 +- 不发送真实设备命令。 +- 不修改生产 `ruoyi-*` 模块。 +- 不启用 SHADOW 或 ACTIVE 控制模式。 + +## 已覆盖行为 + +- 每个调度周期只执行一次业务任务准入。 +- 在线、自动、空闲、允许下发且电量大于 30 的车辆才可参与分配。 +- 同层、楼层差、Y 距离、X 距离、车辆编号的稳定选车规则。 +- 其他车辆剩余路径和离线车辆当前位置锁定。 +- 同通道阻挡、高优先级避让任务和避让去重。 +- 模拟协议失败、下一周期重试。 +- 队列 `0/1 → 1/3 → 1/4` 状态流转、完成归档和占用释放。 +- 无路可达时返回 HOLD,而不是抛出空指针。 + +## 运行测试 + +```bash +mvn test +``` + +测试必须由 Maven Surefire 实际发现并执行;验收标准为测试数大于 0、Failures 为 0、Errors 为 0。 + +## 运行控制台模拟 + +```bash +mvn exec:java +``` + +输出事件包括: + +- `CYCLE`:调度周期开始 +- `ASSIGN`:任务选车和裁决理由 +- `PATH`:规划成功的路径 +- `AVOID`:创建避让队列 +- `HOLD`:交通或协议原因导致等待 +- `SEND`:模拟协议下发成功 +- `MOVE`:模拟车辆推进 +- `COMPLETE`:队列完成并归档 +- `SUMMARY`:最终周期、活动队列、历史和协议尝试次数 diff --git a/javaTest/src/main/java/com/ft546/wcs/demo/DemoApplication.java b/javaTest/src/main/java/com/ft546/wcs/demo/DemoApplication.java new file mode 100644 index 0000000..a7f35f7 --- /dev/null +++ b/javaTest/src/main/java/com/ft546/wcs/demo/DemoApplication.java @@ -0,0 +1,109 @@ +package com.ft546.wcs.demo; + +import com.ft546.wcs.demo.center.CycleResult; +import com.ft546.wcs.demo.center.LegacyDispatchCenter; +import com.ft546.wcs.demo.center.QueueAdmissionService; +import com.ft546.wcs.demo.center.VehicleAssignmentService; +import com.ft546.wcs.demo.model.DispatchTask; +import com.ft546.wcs.demo.model.Site; +import com.ft546.wcs.demo.model.VehicleSnapshot; +import com.ft546.wcs.demo.repository.InMemoryWcsState; +import com.ft546.wcs.demo.simulation.SimulatedProtocolAdapter; +import com.ft546.wcs.demo.traffic.GridMap; +import com.ft546.wcs.demo.traffic.GridPathPlanner; +import com.ft546.wcs.demo.traffic.TrafficController; + +import java.io.PrintStream; + +public final class DemoApplication { + + private DemoApplication() { + } + + public static void main(String[] args) { + runScenario(System.out); + } + + public static void runScenario(PrintStream out) { + if (out == null) { + throw new IllegalArgumentException("output must not be null"); + } + + GridMap map = createMap(); + InMemoryWcsState state = new InMemoryWcsState(); + state.addVehicle(vehicle("3", "V3")); + state.addVehicle(vehicle("2", "B")); + state.addVehicle(vehicle("1", "A")); + state.addTask(new DispatchTask( + "TASK-OUT-1", + DispatchTask.TaskType.OUTBOUND, + "A", + "TARGET", + 1, + 2 + )); + + QueueAdmissionService admission = new QueueAdmissionService( + new VehicleAssignmentService(), + map + ); + SimulatedProtocolAdapter protocol = new SimulatedProtocolAdapter(); + protocol.failNextSend("simulated first PLC rejection"); + LegacyDispatchCenter center = new LegacyDispatchCenter( + state, + admission, + new TrafficController(map, new GridPathPlanner()), + protocol + ); + + out.println("FT546 WCS LEGACY DISPATCH AND TRAFFIC DEMO"); + out.println("MODE LEGACY / IO DISABLED / IN-MEMORY ONLY"); + + for (int cycle = 0; cycle < 6; cycle++) { + printCycle(out, center.runCycle()); + } + + state.addTask(new DispatchTask( + "TASK-NO-PATH", + DispatchTask.TaskType.RELOCATION, + "TARGET", + "ISOLATED", + 1, + 1 + )); + printCycle(out, center.runCycle()); + + out.println( + "SUMMARY cycles=" + state.getCycleNumber() + + " activeQueues=" + state.getActiveQueues().size() + + " history=" + state.getHistory().size() + + " protocolAttempts=" + protocol.getAttemptedQueueIds().size() + ); + } + + private static void printCycle(PrintStream out, CycleResult result) { + for (String event : result.getEvents()) { + out.println(event); + } + } + + private static GridMap createMap() { + GridMap map = new GridMap(); + map.addSite(new Site("A", 1, 0, 0, "START")); + map.addSite(new Site("B", 1, 1, 0, "AISLE-1")); + map.addSite(new Site("TARGET", 1, 2, 0, "AISLE-1")); + map.addSite(new Site("CACHE", 1, 1, 1, "CACHE")); + map.addSite(new Site("V3", 1, 0, 1, "V3-CHANNEL")); + map.addSite(new Site("ISOLATED", 1, 9, 9, "ISLAND")); + map.connect("A", "B"); + map.connect("B", "TARGET"); + map.connect("B", "CACHE"); + map.connect("A", "V3"); + map.setCacheSite("AISLE-1", "CACHE"); + return map; + } + + private static VehicleSnapshot vehicle(String id, String site) { + return new VehicleSnapshot(id, 1, site, 5, 1, 80, true, true, false); + } +} diff --git a/javaTest/src/test/java/com/ft546/wcs/demo/DemoScenarioTest.java b/javaTest/src/test/java/com/ft546/wcs/demo/DemoScenarioTest.java new file mode 100644 index 0000000..4db50a7 --- /dev/null +++ b/javaTest/src/test/java/com/ft546/wcs/demo/DemoScenarioTest.java @@ -0,0 +1,32 @@ +package com.ft546.wcs.demo; + +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class DemoScenarioTest { + + @Test + void executableScenarioShowsAssignmentAvoidanceRetryCompletionAndNoPathHold() + throws Exception { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + + DemoApplication.runScenario(new PrintStream(output, true, "UTF-8")); + + String text = new String(output.toByteArray(), StandardCharsets.UTF_8); + assertTrue(text.contains("CYCLE")); + assertTrue(text.contains("ASSIGN")); + assertTrue(text.contains("PATH")); + assertTrue(text.contains("AVOID")); + assertTrue(text.contains("HOLD HOLD_PROTOCOL_FAILURE")); + assertTrue(text.contains("HOLD HOLD_CHANNEL_BLOCKED")); + assertTrue(text.contains("HOLD HOLD_NO_PATH")); + assertTrue(text.contains("SEND")); + assertTrue(text.contains("COMPLETE")); + assertTrue(text.contains("SUMMARY cycles=7 activeQueues=1 history=2")); + } +}