From ff384cc019e07678fc8a89604b6efb7f6229e03a Mon Sep 17 00:00:00 2001 From: zgw <13623675548@163.com> Date: Tue, 11 Aug 2026 13:01:21 +0800 Subject: [PATCH] fix(ft546): enforce shuttle motion command gate --- .../shuttle/Ft546LegacySrgvAdapter.java | 24 +++++---- .../shuttle/Ft546SrgvCommandEncoder.java | 8 +-- .../shuttle/Ft546LegacySrgvAdapterTest.java | 51 ++++++++++++++----- 3 files changed, 55 insertions(+), 28 deletions(-) diff --git a/ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/shuttle/Ft546LegacySrgvAdapter.java b/ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/shuttle/Ft546LegacySrgvAdapter.java index 6791b9b..71e5655 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/shuttle/Ft546LegacySrgvAdapter.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/shuttle/Ft546LegacySrgvAdapter.java @@ -22,7 +22,7 @@ public final class Ft546LegacySrgvAdapter private final Ft546DeviceRuntimeRegistry registry; private final Ft546DeviceProfile profile; private final CapabilityGateResult capabilities; - private final UnknownCommandGuard unknownCommandGuard; + private final MotionCommandGuard motionCommandGuard; private final SnapshotContextProvider snapshotContextProvider; private final Ft546SrgvCommandEncoder encoder = new Ft546SrgvCommandEncoder(); @@ -36,7 +36,7 @@ public final class Ft546LegacySrgvAdapter Ft546DeviceProfile profile, Ft546CapabilityEvidence evidence, Ft546CapabilityGate capabilityGate, - UnknownCommandGuard unknownCommandGuard, + MotionCommandGuard motionCommandGuard, SnapshotContextProvider contextProvider) { this.registry = Objects.requireNonNull(registry, "registry"); this.profile = copyProfile(Objects.requireNonNull(profile, @@ -46,12 +46,13 @@ public final class Ft546LegacySrgvAdapter if (!capabilities.isPublishable()) { throw new IllegalStateException(capabilities.getReasonCode()); } - if (!capabilities.isEnabled("POINT_TO_POINT_COMMAND")) { + if (!capabilities.isEnabled("POINT_TO_POINT_COMMAND") + && !capabilities.isEnabled("TWO_STAGE_ATOMIC_TASK")) { throw new IllegalStateException( - "POINT_TO_POINT_COMMAND is required"); + "a legacy shuttle movement capability is required"); } - this.unknownCommandGuard = Objects.requireNonNull( - unknownCommandGuard, "unknownCommandGuard"); + this.motionCommandGuard = Objects.requireNonNull( + motionCommandGuard, "motionCommandGuard"); this.snapshotContextProvider = Objects.requireNonNull(contextProvider, "contextProvider"); } @@ -65,10 +66,11 @@ public final class Ft546LegacySrgvAdapter "profile belongs to another device"); } if (locallyUnknownDevices.contains(envelope.getDeviceId()) - || unknownCommandGuard.hasActiveUnknownCommand( - envelope.getDeviceId())) { + || motionCommandGuard.hasBlockingMotionCommand( + envelope.getDeviceId(), envelope.getCommandId())) { return SendReceipt.rejected(receiptId, - "device has an active UNKNOWN command"); + "device has another active motion command or an " + + "unresolved UNKNOWN command"); } final Ft546SrgvCommandEncoder.EncodedCommand command; @@ -147,8 +149,8 @@ public final class Ft546LegacySrgvAdapter } @FunctionalInterface - public interface UnknownCommandGuard { - boolean hasActiveUnknownCommand(String deviceId); + public interface MotionCommandGuard { + boolean hasBlockingMotionCommand(String deviceId, String commandId); } @FunctionalInterface diff --git a/ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/shuttle/Ft546SrgvCommandEncoder.java b/ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/shuttle/Ft546SrgvCommandEncoder.java index e1597b5..7abfe4c 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/shuttle/Ft546SrgvCommandEncoder.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/project/ft546/device/shuttle/Ft546SrgvCommandEncoder.java @@ -20,10 +20,6 @@ final class Ft546SrgvCommandEncoder { throw new IllegalArgumentException( "legacy shuttle supports MOVE_ROUTE only"); } - if (!capabilities.isEnabled("POINT_TO_POINT_COMMAND")) { - throw new IllegalStateException( - "POINT_TO_POINT_COMMAND is not enabled"); - } Map values = parse(envelope.getPayload()); int taskNo = positiveInt(required(values, "taskNo"), "taskNo"); short taskType = shortValue(required(values, "taskType"), @@ -31,6 +27,10 @@ final class Ft546SrgvCommandEncoder { int[] route = route(required(values, "route")); String stageValue = values.get("stage"); if (stageValue == null) { + if (!capabilities.isEnabled("POINT_TO_POINT_COMMAND")) { + throw new IllegalStateException( + "POINT_TO_POINT_COMMAND is not enabled"); + } return main(profile, values, taskNo, taskType, route); } if (!capabilities.isEnabled("TWO_STAGE_ATOMIC_TASK")) { diff --git a/ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device/shuttle/Ft546LegacySrgvAdapterTest.java b/ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device/shuttle/Ft546LegacySrgvAdapterTest.java index cda6e69..18519b2 100644 --- a/ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device/shuttle/Ft546LegacySrgvAdapterTest.java +++ b/ruoyi-admin/src/test/java/com/ruoyi/project/ft546/device/shuttle/Ft546LegacySrgvAdapterTest.java @@ -37,7 +37,7 @@ class Ft546LegacySrgvAdapterTest { JsonNode fixture = fixture("srgv-point-to-point.json"); FakePlcSrgvRead runtime = connectedRuntime("1"); Ft546LegacySrgvAdapter adapter = adapter(runtime, pointProfile(), - null, deviceId -> false); + null, (deviceId, commandId) -> false); SendReceipt receipt = adapter.send(envelope("CMD-1", fixture.get("payload").asText())); @@ -54,7 +54,7 @@ class Ft546LegacySrgvAdapterTest { FakePlcSrgvRead runtime = connectedRuntime("1"); runtime.throwAfterAddress = "4000"; Ft546LegacySrgvAdapter adapter = adapter(runtime, pointProfile(), - null, deviceId -> false); + null, (deviceId, commandId) -> false); SendReceipt first = adapter.send(envelope("CMD-1", fixture.get("payload").asText())); @@ -72,7 +72,7 @@ class Ft546LegacySrgvAdapterTest { void disconnectedRuntimeIsNotSentWithoutAnyPlcWrite() { FakePlcSrgvRead runtime = runtime("1"); Ft546LegacySrgvAdapter adapter = adapter(runtime, pointProfile(), - null, deviceId -> false); + null, (deviceId, commandId) -> false); SendReceipt receipt = adapter.send(envelope("CMD-1", "taskNo=30001|taskType=1|route=070501>070502")); @@ -86,7 +86,7 @@ class Ft546LegacySrgvAdapterTest { FakePlcSrgvRead runtime = connectedRuntime("1"); runtime.returnFalseAfterAddress = "4004"; Ft546LegacySrgvAdapter adapter = adapter(runtime, pointProfile(), - null, deviceId -> false); + null, (deviceId, commandId) -> false); SendReceipt receipt = adapter.send(envelope("CMD-1", "taskNo=30001|taskType=1|route=070501>070502")); @@ -104,7 +104,7 @@ class Ft546LegacySrgvAdapterTest { profile.getRegisters().put("crossFloorFlag", "4020"); profile.getRegisters().put("crossFloorTarget", "4022"); Ft546LegacySrgvAdapter adapter = adapter(runtime, profile, - null, deviceId -> false); + null, (deviceId, commandId) -> false); SendReceipt receipt = adapter.send(envelope("CMD-XF", fixture.get("crossFloorPayload").asText())); @@ -119,7 +119,7 @@ class Ft546LegacySrgvAdapterTest { FakePlcSrgvRead runtime = connectedRuntime("1"); Ft546DeviceProfile profile = pointProfile(); Ft546LegacySrgvAdapter adapter = adapter(runtime, profile, - null, deviceId -> false); + null, (deviceId, commandId) -> false); profile.getRegisters().put("path", "9999"); SendReceipt receipt = adapter.send(envelope("CMD-1", @@ -130,15 +130,21 @@ class Ft546LegacySrgvAdapterTest { } @Test - void coreReportedUnknownCommandRejectsAParallelMove() { + void coreReportedActiveCommandRejectsAParallelMove() { FakePlcSrgvRead runtime = connectedRuntime("1"); + List checkedCommands = new ArrayList(); Ft546LegacySrgvAdapter adapter = adapter(runtime, pointProfile(), - null, deviceId -> true); + null, (deviceId, commandId) -> { + checkedCommands.add(deviceId + ":" + commandId); + return true; + }); SendReceipt receipt = adapter.send(envelope("CMD-1", "taskNo=30001|taskType=1|route=070501>070502")); assertEquals(SendReceipt.Status.REJECTED, receipt.getStatus()); + assertEquals(Collections.singletonList("1:CMD-1"), + checkedCommands); assertTrue(runtime.writes.isEmpty()); } @@ -147,7 +153,8 @@ class Ft546LegacySrgvAdapterTest { FakePlcSrgvRead runtime = connectedRuntime("1"); assertThrows(IllegalStateException.class, () -> adapter(runtime, - twoStageProfile(), null, deviceId -> false)); + twoStageProfile(), null, + (deviceId, commandId) -> false)); assertTrue(runtime.writes.isEmpty()); } @@ -160,13 +167,13 @@ class Ft546LegacySrgvAdapterTest { FakePlcSrgvRead stageOneRuntime = connectedRuntime("1"); Ft546LegacySrgvAdapter stageOneAdapter = adapter(stageOneRuntime, - profile, evidence, deviceId -> false); + profile, evidence, (deviceId, commandId) -> false); SendReceipt stageOne = stageOneAdapter.send(envelope("CMD-STAGE-1", fixture.get("stageOnePayload").asText())); FakePlcSrgvRead stageTwoRuntime = connectedRuntime("1"); Ft546LegacySrgvAdapter stageTwoAdapter = adapter(stageTwoRuntime, - profile, evidence, deviceId -> false); + profile, evidence, (deviceId, commandId) -> false); SendReceipt stageTwo = stageTwoAdapter.send(envelope("CMD-STAGE-2", fixture.get("stageTwoPayload").asText())); @@ -176,6 +183,24 @@ class Ft546LegacySrgvAdapterTest { assertWrites(fixture.get("stageTwoWrites"), stageTwoRuntime.writes); } + @Test + void evidenceGatedTwoStageProfileDoesNotRequirePointToPointCapability() + throws Exception { + JsonNode fixture = fixture("srgv-two-stage-gate.json"); + Ft546DeviceProfile profile = twoStageProfile(); + profile.getCapabilities().remove("POINT_TO_POINT_COMMAND"); + FakePlcSrgvRead runtime = connectedRuntime("1"); + Ft546LegacySrgvAdapter adapter = adapter(runtime, profile, + passingEvidence(profile), + (deviceId, commandId) -> false); + + SendReceipt receipt = adapter.send(envelope("CMD-STAGE-ONLY", + fixture.get("stageOnePayload").asText())); + + assertEquals(SendReceipt.Status.ACCEPTED, receipt.getStatus()); + assertWrites(fixture.get("stageOneWrites"), runtime.writes); + } + @Test void mapsLegacyFactsWithoutFabricatingDeviceSequenceOrBootSession() { FakePlcSrgvRead runtime = connectedRuntime("1"); @@ -216,12 +241,12 @@ class Ft546LegacySrgvAdapterTest { private static Ft546LegacySrgvAdapter adapter( FakePlcSrgvRead runtime, Ft546DeviceProfile profile, Ft546CapabilityEvidence evidence, - Ft546LegacySrgvAdapter.UnknownCommandGuard unknownGuard) { + Ft546LegacySrgvAdapter.MotionCommandGuard motionGuard) { Ft546DeviceRuntimeRegistry registry = new Ft546DeviceRuntimeRegistry(); registry.register("1", runtime); return new Ft546LegacySrgvAdapter(registry, profile, evidence, - new Ft546CapabilityGate(() -> NOW), unknownGuard, + new Ft546CapabilityGate(() -> NOW), motionGuard, deviceId -> new Ft546SrgvSnapshotMapper.Context( "SNAP-" + deviceId, "KEY-" + deviceId, 9L, 17L, NOW));