fix(ft546): enforce shuttle motion command gate
This commit is contained in:
@@ -22,7 +22,7 @@ public final class Ft546LegacySrgvAdapter
|
|||||||
private final Ft546DeviceRuntimeRegistry registry;
|
private final Ft546DeviceRuntimeRegistry registry;
|
||||||
private final Ft546DeviceProfile profile;
|
private final Ft546DeviceProfile profile;
|
||||||
private final CapabilityGateResult capabilities;
|
private final CapabilityGateResult capabilities;
|
||||||
private final UnknownCommandGuard unknownCommandGuard;
|
private final MotionCommandGuard motionCommandGuard;
|
||||||
private final SnapshotContextProvider snapshotContextProvider;
|
private final SnapshotContextProvider snapshotContextProvider;
|
||||||
private final Ft546SrgvCommandEncoder encoder =
|
private final Ft546SrgvCommandEncoder encoder =
|
||||||
new Ft546SrgvCommandEncoder();
|
new Ft546SrgvCommandEncoder();
|
||||||
@@ -36,7 +36,7 @@ public final class Ft546LegacySrgvAdapter
|
|||||||
Ft546DeviceProfile profile,
|
Ft546DeviceProfile profile,
|
||||||
Ft546CapabilityEvidence evidence,
|
Ft546CapabilityEvidence evidence,
|
||||||
Ft546CapabilityGate capabilityGate,
|
Ft546CapabilityGate capabilityGate,
|
||||||
UnknownCommandGuard unknownCommandGuard,
|
MotionCommandGuard motionCommandGuard,
|
||||||
SnapshotContextProvider contextProvider) {
|
SnapshotContextProvider contextProvider) {
|
||||||
this.registry = Objects.requireNonNull(registry, "registry");
|
this.registry = Objects.requireNonNull(registry, "registry");
|
||||||
this.profile = copyProfile(Objects.requireNonNull(profile,
|
this.profile = copyProfile(Objects.requireNonNull(profile,
|
||||||
@@ -46,12 +46,13 @@ public final class Ft546LegacySrgvAdapter
|
|||||||
if (!capabilities.isPublishable()) {
|
if (!capabilities.isPublishable()) {
|
||||||
throw new IllegalStateException(capabilities.getReasonCode());
|
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(
|
throw new IllegalStateException(
|
||||||
"POINT_TO_POINT_COMMAND is required");
|
"a legacy shuttle movement capability is required");
|
||||||
}
|
}
|
||||||
this.unknownCommandGuard = Objects.requireNonNull(
|
this.motionCommandGuard = Objects.requireNonNull(
|
||||||
unknownCommandGuard, "unknownCommandGuard");
|
motionCommandGuard, "motionCommandGuard");
|
||||||
this.snapshotContextProvider = Objects.requireNonNull(contextProvider,
|
this.snapshotContextProvider = Objects.requireNonNull(contextProvider,
|
||||||
"contextProvider");
|
"contextProvider");
|
||||||
}
|
}
|
||||||
@@ -65,10 +66,11 @@ public final class Ft546LegacySrgvAdapter
|
|||||||
"profile belongs to another device");
|
"profile belongs to another device");
|
||||||
}
|
}
|
||||||
if (locallyUnknownDevices.contains(envelope.getDeviceId())
|
if (locallyUnknownDevices.contains(envelope.getDeviceId())
|
||||||
|| unknownCommandGuard.hasActiveUnknownCommand(
|
|| motionCommandGuard.hasBlockingMotionCommand(
|
||||||
envelope.getDeviceId())) {
|
envelope.getDeviceId(), envelope.getCommandId())) {
|
||||||
return SendReceipt.rejected(receiptId,
|
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;
|
final Ft546SrgvCommandEncoder.EncodedCommand command;
|
||||||
@@ -147,8 +149,8 @@ public final class Ft546LegacySrgvAdapter
|
|||||||
}
|
}
|
||||||
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface UnknownCommandGuard {
|
public interface MotionCommandGuard {
|
||||||
boolean hasActiveUnknownCommand(String deviceId);
|
boolean hasBlockingMotionCommand(String deviceId, String commandId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
|
|||||||
@@ -20,10 +20,6 @@ final class Ft546SrgvCommandEncoder {
|
|||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"legacy shuttle supports MOVE_ROUTE only");
|
"legacy shuttle supports MOVE_ROUTE only");
|
||||||
}
|
}
|
||||||
if (!capabilities.isEnabled("POINT_TO_POINT_COMMAND")) {
|
|
||||||
throw new IllegalStateException(
|
|
||||||
"POINT_TO_POINT_COMMAND is not enabled");
|
|
||||||
}
|
|
||||||
Map<String, String> values = parse(envelope.getPayload());
|
Map<String, String> values = parse(envelope.getPayload());
|
||||||
int taskNo = positiveInt(required(values, "taskNo"), "taskNo");
|
int taskNo = positiveInt(required(values, "taskNo"), "taskNo");
|
||||||
short taskType = shortValue(required(values, "taskType"),
|
short taskType = shortValue(required(values, "taskType"),
|
||||||
@@ -31,6 +27,10 @@ final class Ft546SrgvCommandEncoder {
|
|||||||
int[] route = route(required(values, "route"));
|
int[] route = route(required(values, "route"));
|
||||||
String stageValue = values.get("stage");
|
String stageValue = values.get("stage");
|
||||||
if (stageValue == null) {
|
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);
|
return main(profile, values, taskNo, taskType, route);
|
||||||
}
|
}
|
||||||
if (!capabilities.isEnabled("TWO_STAGE_ATOMIC_TASK")) {
|
if (!capabilities.isEnabled("TWO_STAGE_ATOMIC_TASK")) {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class Ft546LegacySrgvAdapterTest {
|
|||||||
JsonNode fixture = fixture("srgv-point-to-point.json");
|
JsonNode fixture = fixture("srgv-point-to-point.json");
|
||||||
FakePlcSrgvRead runtime = connectedRuntime("1");
|
FakePlcSrgvRead runtime = connectedRuntime("1");
|
||||||
Ft546LegacySrgvAdapter adapter = adapter(runtime, pointProfile(),
|
Ft546LegacySrgvAdapter adapter = adapter(runtime, pointProfile(),
|
||||||
null, deviceId -> false);
|
null, (deviceId, commandId) -> false);
|
||||||
|
|
||||||
SendReceipt receipt = adapter.send(envelope("CMD-1",
|
SendReceipt receipt = adapter.send(envelope("CMD-1",
|
||||||
fixture.get("payload").asText()));
|
fixture.get("payload").asText()));
|
||||||
@@ -54,7 +54,7 @@ class Ft546LegacySrgvAdapterTest {
|
|||||||
FakePlcSrgvRead runtime = connectedRuntime("1");
|
FakePlcSrgvRead runtime = connectedRuntime("1");
|
||||||
runtime.throwAfterAddress = "4000";
|
runtime.throwAfterAddress = "4000";
|
||||||
Ft546LegacySrgvAdapter adapter = adapter(runtime, pointProfile(),
|
Ft546LegacySrgvAdapter adapter = adapter(runtime, pointProfile(),
|
||||||
null, deviceId -> false);
|
null, (deviceId, commandId) -> false);
|
||||||
|
|
||||||
SendReceipt first = adapter.send(envelope("CMD-1",
|
SendReceipt first = adapter.send(envelope("CMD-1",
|
||||||
fixture.get("payload").asText()));
|
fixture.get("payload").asText()));
|
||||||
@@ -72,7 +72,7 @@ class Ft546LegacySrgvAdapterTest {
|
|||||||
void disconnectedRuntimeIsNotSentWithoutAnyPlcWrite() {
|
void disconnectedRuntimeIsNotSentWithoutAnyPlcWrite() {
|
||||||
FakePlcSrgvRead runtime = runtime("1");
|
FakePlcSrgvRead runtime = runtime("1");
|
||||||
Ft546LegacySrgvAdapter adapter = adapter(runtime, pointProfile(),
|
Ft546LegacySrgvAdapter adapter = adapter(runtime, pointProfile(),
|
||||||
null, deviceId -> false);
|
null, (deviceId, commandId) -> false);
|
||||||
|
|
||||||
SendReceipt receipt = adapter.send(envelope("CMD-1",
|
SendReceipt receipt = adapter.send(envelope("CMD-1",
|
||||||
"taskNo=30001|taskType=1|route=070501>070502"));
|
"taskNo=30001|taskType=1|route=070501>070502"));
|
||||||
@@ -86,7 +86,7 @@ class Ft546LegacySrgvAdapterTest {
|
|||||||
FakePlcSrgvRead runtime = connectedRuntime("1");
|
FakePlcSrgvRead runtime = connectedRuntime("1");
|
||||||
runtime.returnFalseAfterAddress = "4004";
|
runtime.returnFalseAfterAddress = "4004";
|
||||||
Ft546LegacySrgvAdapter adapter = adapter(runtime, pointProfile(),
|
Ft546LegacySrgvAdapter adapter = adapter(runtime, pointProfile(),
|
||||||
null, deviceId -> false);
|
null, (deviceId, commandId) -> false);
|
||||||
|
|
||||||
SendReceipt receipt = adapter.send(envelope("CMD-1",
|
SendReceipt receipt = adapter.send(envelope("CMD-1",
|
||||||
"taskNo=30001|taskType=1|route=070501>070502"));
|
"taskNo=30001|taskType=1|route=070501>070502"));
|
||||||
@@ -104,7 +104,7 @@ class Ft546LegacySrgvAdapterTest {
|
|||||||
profile.getRegisters().put("crossFloorFlag", "4020");
|
profile.getRegisters().put("crossFloorFlag", "4020");
|
||||||
profile.getRegisters().put("crossFloorTarget", "4022");
|
profile.getRegisters().put("crossFloorTarget", "4022");
|
||||||
Ft546LegacySrgvAdapter adapter = adapter(runtime, profile,
|
Ft546LegacySrgvAdapter adapter = adapter(runtime, profile,
|
||||||
null, deviceId -> false);
|
null, (deviceId, commandId) -> false);
|
||||||
|
|
||||||
SendReceipt receipt = adapter.send(envelope("CMD-XF",
|
SendReceipt receipt = adapter.send(envelope("CMD-XF",
|
||||||
fixture.get("crossFloorPayload").asText()));
|
fixture.get("crossFloorPayload").asText()));
|
||||||
@@ -119,7 +119,7 @@ class Ft546LegacySrgvAdapterTest {
|
|||||||
FakePlcSrgvRead runtime = connectedRuntime("1");
|
FakePlcSrgvRead runtime = connectedRuntime("1");
|
||||||
Ft546DeviceProfile profile = pointProfile();
|
Ft546DeviceProfile profile = pointProfile();
|
||||||
Ft546LegacySrgvAdapter adapter = adapter(runtime, profile,
|
Ft546LegacySrgvAdapter adapter = adapter(runtime, profile,
|
||||||
null, deviceId -> false);
|
null, (deviceId, commandId) -> false);
|
||||||
profile.getRegisters().put("path", "9999");
|
profile.getRegisters().put("path", "9999");
|
||||||
|
|
||||||
SendReceipt receipt = adapter.send(envelope("CMD-1",
|
SendReceipt receipt = adapter.send(envelope("CMD-1",
|
||||||
@@ -130,15 +130,21 @@ class Ft546LegacySrgvAdapterTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void coreReportedUnknownCommandRejectsAParallelMove() {
|
void coreReportedActiveCommandRejectsAParallelMove() {
|
||||||
FakePlcSrgvRead runtime = connectedRuntime("1");
|
FakePlcSrgvRead runtime = connectedRuntime("1");
|
||||||
|
List<String> checkedCommands = new ArrayList<String>();
|
||||||
Ft546LegacySrgvAdapter adapter = adapter(runtime, pointProfile(),
|
Ft546LegacySrgvAdapter adapter = adapter(runtime, pointProfile(),
|
||||||
null, deviceId -> true);
|
null, (deviceId, commandId) -> {
|
||||||
|
checkedCommands.add(deviceId + ":" + commandId);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
SendReceipt receipt = adapter.send(envelope("CMD-1",
|
SendReceipt receipt = adapter.send(envelope("CMD-1",
|
||||||
"taskNo=30001|taskType=1|route=070501>070502"));
|
"taskNo=30001|taskType=1|route=070501>070502"));
|
||||||
|
|
||||||
assertEquals(SendReceipt.Status.REJECTED, receipt.getStatus());
|
assertEquals(SendReceipt.Status.REJECTED, receipt.getStatus());
|
||||||
|
assertEquals(Collections.singletonList("1:CMD-1"),
|
||||||
|
checkedCommands);
|
||||||
assertTrue(runtime.writes.isEmpty());
|
assertTrue(runtime.writes.isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,7 +153,8 @@ class Ft546LegacySrgvAdapterTest {
|
|||||||
FakePlcSrgvRead runtime = connectedRuntime("1");
|
FakePlcSrgvRead runtime = connectedRuntime("1");
|
||||||
|
|
||||||
assertThrows(IllegalStateException.class, () -> adapter(runtime,
|
assertThrows(IllegalStateException.class, () -> adapter(runtime,
|
||||||
twoStageProfile(), null, deviceId -> false));
|
twoStageProfile(), null,
|
||||||
|
(deviceId, commandId) -> false));
|
||||||
assertTrue(runtime.writes.isEmpty());
|
assertTrue(runtime.writes.isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,13 +167,13 @@ class Ft546LegacySrgvAdapterTest {
|
|||||||
|
|
||||||
FakePlcSrgvRead stageOneRuntime = connectedRuntime("1");
|
FakePlcSrgvRead stageOneRuntime = connectedRuntime("1");
|
||||||
Ft546LegacySrgvAdapter stageOneAdapter = adapter(stageOneRuntime,
|
Ft546LegacySrgvAdapter stageOneAdapter = adapter(stageOneRuntime,
|
||||||
profile, evidence, deviceId -> false);
|
profile, evidence, (deviceId, commandId) -> false);
|
||||||
SendReceipt stageOne = stageOneAdapter.send(envelope("CMD-STAGE-1",
|
SendReceipt stageOne = stageOneAdapter.send(envelope("CMD-STAGE-1",
|
||||||
fixture.get("stageOnePayload").asText()));
|
fixture.get("stageOnePayload").asText()));
|
||||||
|
|
||||||
FakePlcSrgvRead stageTwoRuntime = connectedRuntime("1");
|
FakePlcSrgvRead stageTwoRuntime = connectedRuntime("1");
|
||||||
Ft546LegacySrgvAdapter stageTwoAdapter = adapter(stageTwoRuntime,
|
Ft546LegacySrgvAdapter stageTwoAdapter = adapter(stageTwoRuntime,
|
||||||
profile, evidence, deviceId -> false);
|
profile, evidence, (deviceId, commandId) -> false);
|
||||||
SendReceipt stageTwo = stageTwoAdapter.send(envelope("CMD-STAGE-2",
|
SendReceipt stageTwo = stageTwoAdapter.send(envelope("CMD-STAGE-2",
|
||||||
fixture.get("stageTwoPayload").asText()));
|
fixture.get("stageTwoPayload").asText()));
|
||||||
|
|
||||||
@@ -176,6 +183,24 @@ class Ft546LegacySrgvAdapterTest {
|
|||||||
assertWrites(fixture.get("stageTwoWrites"), stageTwoRuntime.writes);
|
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
|
@Test
|
||||||
void mapsLegacyFactsWithoutFabricatingDeviceSequenceOrBootSession() {
|
void mapsLegacyFactsWithoutFabricatingDeviceSequenceOrBootSession() {
|
||||||
FakePlcSrgvRead runtime = connectedRuntime("1");
|
FakePlcSrgvRead runtime = connectedRuntime("1");
|
||||||
@@ -216,12 +241,12 @@ class Ft546LegacySrgvAdapterTest {
|
|||||||
private static Ft546LegacySrgvAdapter adapter(
|
private static Ft546LegacySrgvAdapter adapter(
|
||||||
FakePlcSrgvRead runtime, Ft546DeviceProfile profile,
|
FakePlcSrgvRead runtime, Ft546DeviceProfile profile,
|
||||||
Ft546CapabilityEvidence evidence,
|
Ft546CapabilityEvidence evidence,
|
||||||
Ft546LegacySrgvAdapter.UnknownCommandGuard unknownGuard) {
|
Ft546LegacySrgvAdapter.MotionCommandGuard motionGuard) {
|
||||||
Ft546DeviceRuntimeRegistry registry =
|
Ft546DeviceRuntimeRegistry registry =
|
||||||
new Ft546DeviceRuntimeRegistry();
|
new Ft546DeviceRuntimeRegistry();
|
||||||
registry.register("1", runtime);
|
registry.register("1", runtime);
|
||||||
return new Ft546LegacySrgvAdapter(registry, profile, evidence,
|
return new Ft546LegacySrgvAdapter(registry, profile, evidence,
|
||||||
new Ft546CapabilityGate(() -> NOW), unknownGuard,
|
new Ft546CapabilityGate(() -> NOW), motionGuard,
|
||||||
deviceId -> new Ft546SrgvSnapshotMapper.Context(
|
deviceId -> new Ft546SrgvSnapshotMapper.Context(
|
||||||
"SNAP-" + deviceId, "KEY-" + deviceId,
|
"SNAP-" + deviceId, "KEY-" + deviceId,
|
||||||
9L, 17L, NOW));
|
9L, 17L, NOW));
|
||||||
|
|||||||
Reference in New Issue
Block a user