refactor(ft546): share pure PLC command encoding
This commit is contained in:
@@ -12,7 +12,7 @@ import com.ruoyi.common.domain.PlcConnectInfo;
|
||||
import com.ruoyi.device.srgv.domain.DeviceSrgv;
|
||||
import com.ruoyi.project.ft546.config.Ft546CapabilityGate;
|
||||
import com.ruoyi.project.ft546.config.Ft546CapabilityEvidence;
|
||||
import com.ruoyi.project.ft546.config.Ft546DeviceProfile;
|
||||
import com.ruoyi.ft546.protocol.Ft546DeviceProfile;
|
||||
import com.ruoyi.project.ft546.config.Ft546WcsProperties;
|
||||
import com.ruoyi.project.ft546.device.Ft546DeviceRuntimeRegistry;
|
||||
import com.ruoyi.project.ft546.device.shuttle.Ft546LegacySrgvAdapter;
|
||||
@@ -614,27 +614,27 @@ class Ft546StandardWcsEndToEndTest {
|
||||
profile.setDeviceType("SHUTTLE");
|
||||
profile.setAdapterType("FT546_LEGACY_SRGV");
|
||||
profile.setProfileVersion(1L);
|
||||
profile.getCapabilities().add("POINT_TO_POINT_COMMAND");
|
||||
profile.getRegisters().put("taskNo", "4000");
|
||||
profile.getRegisters().put("taskType", "4002");
|
||||
profile.getRegisters().put("path", "4004");
|
||||
profile.getRegisters().put("trigger", "4021");
|
||||
profile.getRegisters().put("crossFloorFlag", "4020");
|
||||
profile.getRegisters().put("crossFloorTarget", "4022");
|
||||
profile.getRegisters().put("status", "4068");
|
||||
profile.getRegisters().put("execution", "4060");
|
||||
profile.addCapability("POINT_TO_POINT_COMMAND");
|
||||
profile.putRegister("taskNo", "4000");
|
||||
profile.putRegister("taskType", "4002");
|
||||
profile.putRegister("path", "4004");
|
||||
profile.putRegister("trigger", "4021");
|
||||
profile.putRegister("crossFloorFlag", "4020");
|
||||
profile.putRegister("crossFloorTarget", "4022");
|
||||
profile.putRegister("status", "4068");
|
||||
profile.putRegister("execution", "4060");
|
||||
return profile;
|
||||
}
|
||||
|
||||
private static Ft546DeviceProfile integrationProfile() {
|
||||
Ft546DeviceProfile profile = pointProfile();
|
||||
profile.getCapabilities().add("TWO_STAGE_ATOMIC_TASK");
|
||||
profile.addCapability("TWO_STAGE_ATOMIC_TASK");
|
||||
profile.setProtocolCorpusHash("sha256:ft546-two-stage-v1");
|
||||
profile.getRegisters().put("secondTaskNo", "4400");
|
||||
profile.getRegisters().put("secondTaskType", "4402");
|
||||
profile.getRegisters().put("secondPath", "4404");
|
||||
profile.getRegisters().put("secondStageFlag", "4422");
|
||||
profile.getRegisters().put("secondComplete", "4423");
|
||||
profile.putRegister("secondTaskNo", "4400");
|
||||
profile.putRegister("secondTaskType", "4402");
|
||||
profile.putRegister("secondPath", "4404");
|
||||
profile.putRegister("secondStageFlag", "4422");
|
||||
profile.putRegister("secondComplete", "4423");
|
||||
return profile;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.ruoyi.project.ft546.config;
|
||||
|
||||
import com.ruoyi.ft546.protocol.Ft546DeviceProfile;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -31,7 +33,7 @@ class Ft546ConfigurationValidatorTest {
|
||||
@Test
|
||||
void rejectsLegacyProfileThatClaimsPrepareCommit() {
|
||||
Ft546DeviceProfile profile = legacySrgvProfile();
|
||||
profile.getCapabilities().add("PREPARE_COMMIT_SEGMENT");
|
||||
profile.addCapability("PREPARE_COMMIT_SEGMENT");
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new Ft546ConfigurationValidator().validate(profile));
|
||||
@@ -40,7 +42,7 @@ class Ft546ConfigurationValidatorTest {
|
||||
@Test
|
||||
void rejectsDuplicateRegisterOwnershipWithinOneDevice() {
|
||||
Ft546DeviceProfile profile = legacySrgvProfile();
|
||||
profile.getRegisters().put("taskType", "4000");
|
||||
profile.putRegister("taskType", "4000");
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new Ft546ConfigurationValidator().validate(profile));
|
||||
@@ -49,7 +51,7 @@ class Ft546ConfigurationValidatorTest {
|
||||
@Test
|
||||
void rejectsTwoStageCapabilityWithoutPassingEvidenceForExactProfile() {
|
||||
Ft546DeviceProfile profile = legacySrgvProfile();
|
||||
profile.getCapabilities().add("TWO_STAGE_ATOMIC_TASK");
|
||||
profile.addCapability("TWO_STAGE_ATOMIC_TASK");
|
||||
Ft546CapabilityGate gate = gate();
|
||||
|
||||
CapabilityGateResult result = gate.evaluate(profile, null);
|
||||
@@ -62,7 +64,7 @@ class Ft546ConfigurationValidatorTest {
|
||||
@Test
|
||||
void enablesTwoStageOnlyForPassingUnexpiredEvidenceBoundToProfile() {
|
||||
Ft546DeviceProfile profile = legacySrgvProfile();
|
||||
profile.getCapabilities().add("TWO_STAGE_ATOMIC_TASK");
|
||||
profile.addCapability("TWO_STAGE_ATOMIC_TASK");
|
||||
Ft546CapabilityEvidence evidence = new Ft546CapabilityEvidence(
|
||||
1L, "CORPUS-SHA256", "RUN-1", "COMMIT-1", true,
|
||||
Instant.parse("2026-08-10T00:00:00Z"),
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.ruoyi.project.ft546.device;
|
||||
import com.ruoyi.common.domain.PlcConnectInfo;
|
||||
import com.ruoyi.common.domain.CustomThread;
|
||||
import com.ruoyi.device.srgv.domain.DeviceSrgv;
|
||||
import com.ruoyi.project.ft546.config.Ft546DeviceProfile;
|
||||
import com.ruoyi.ft546.protocol.Ft546DeviceProfile;
|
||||
import com.ruoyi.project.ft546.device.shuttle.Ft546SrgvRuntimeFactory;
|
||||
import com.ruoyi.schedule.ScheduleThread;
|
||||
import com.ruoyi.schedule.plcRead.srgv.PlcSrgvRead;
|
||||
|
||||
@@ -5,7 +5,7 @@ import com.ruoyi.device.cdz.domain.DeviceCdz;
|
||||
import com.ruoyi.device.srgv.domain.DeviceSrgv;
|
||||
import com.ruoyi.device.ssj.domain.DeviceSsj;
|
||||
import com.ruoyi.device.tsj.domain.DeviceTsj;
|
||||
import com.ruoyi.project.ft546.config.Ft546DeviceProfile;
|
||||
import com.ruoyi.ft546.protocol.Ft546DeviceProfile;
|
||||
import com.ruoyi.project.ft546.device.charger.Ft546ChargerAdapter;
|
||||
import com.ruoyi.project.ft546.device.conveyor.Ft546ConveyorAdapter;
|
||||
import com.ruoyi.project.ft546.device.lift.Ft546LiftAdapter;
|
||||
@@ -220,8 +220,8 @@ class Ft546EquipmentAdaptersTest {
|
||||
profile.setDeviceType(deviceType);
|
||||
profile.setAdapterType("FT546_LEGACY_" + deviceType);
|
||||
profile.setProfileVersion(1L);
|
||||
profile.getCapabilities().add(capability);
|
||||
profile.getRegisters().put("command", commandAddress);
|
||||
profile.addCapability(capability);
|
||||
profile.putRegister("command", commandAddress);
|
||||
return profile;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.ruoyi.project.ft546.device.shuttle;
|
||||
|
||||
import com.ruoyi.ft546.protocol.Ft546SrgvCommandIntent;
|
||||
import com.ruoyi.wcs.application.device.DeviceCommandEnvelope;
|
||||
import com.ruoyi.wcs.domain.execution.DeviceCommandType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
class Ft546CommandIntentMapperTest {
|
||||
private final Ft546CommandIntentMapper mapper =
|
||||
new Ft546CommandIntentMapper();
|
||||
|
||||
@Test
|
||||
void oldPayloadUsesDeterministicEvidenceFallbacks() {
|
||||
Ft546SrgvCommandIntent intent = mapper.from(envelope("CMD-1",
|
||||
"KEY-1", "taskNo=30001|taskType=1|"
|
||||
+ "route=070501>070502", 9L));
|
||||
|
||||
assertEquals("1", intent.getDeviceId());
|
||||
assertEquals("CMD-1", intent.getParentTaskId());
|
||||
assertEquals("LEGACY_MOVE_ROUTE", intent.getPhase());
|
||||
assertEquals("KEY-1", intent.getCommandKey());
|
||||
assertEquals(9L, intent.getRouteVersion());
|
||||
assertEquals(30001, intent.getTaskNo());
|
||||
assertEquals((short) 1, intent.getTaskType());
|
||||
assertEquals(Arrays.asList(70501, 70502),
|
||||
intent.getPathPoints());
|
||||
assertNull(intent.getCrossFloorTarget());
|
||||
assertNull(intent.getTwoStageNumber());
|
||||
}
|
||||
|
||||
@Test
|
||||
void explicitEvidenceFieldsOverrideOldFallbacks() {
|
||||
Ft546SrgvCommandIntent intent = mapper.from(envelope("CMD-1",
|
||||
"KEY-1", "parentTaskId=PARENT-1|phase=LOADED_TO_PLACE|"
|
||||
+ "routeVersion=3|taskNo=30001|taskType=8|"
|
||||
+ "route=070501>070502|crossFloorTarget=8", 9L));
|
||||
|
||||
assertEquals("PARENT-1", intent.getParentTaskId());
|
||||
assertEquals("LOADED_TO_PLACE", intent.getPhase());
|
||||
assertEquals(3L, intent.getRouteVersion());
|
||||
assertEquals(Short.valueOf((short) 8),
|
||||
intent.getCrossFloorTarget());
|
||||
}
|
||||
|
||||
@Test
|
||||
void orderIdIsTheOldPayloadParentTaskFallback() {
|
||||
Ft546SrgvCommandIntent intent = mapper.from(envelope("CMD-1",
|
||||
"KEY-1", "orderId=ORDER-1|taskNo=30001|taskType=1|"
|
||||
+ "route=070501>070502", 9L));
|
||||
|
||||
assertEquals("ORDER-1", intent.getParentTaskId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void twoStagePayloadUsesStableStagePhase() {
|
||||
Ft546SrgvCommandIntent intent = mapper.from(envelope("CMD-1",
|
||||
"KEY-1", "taskNo=30001|taskType=9|stage=2|"
|
||||
+ "route=070501>070502", 9L));
|
||||
|
||||
assertEquals("TWO_STAGE_2", intent.getPhase());
|
||||
assertEquals(Integer.valueOf(2), intent.getTwoStageNumber());
|
||||
}
|
||||
|
||||
@Test
|
||||
void irrelevantCrossFloorValueDoesNotRejectOrdinaryOrTwoStagePayload() {
|
||||
Ft546SrgvCommandIntent ordinary = mapper.from(envelope("CMD-1",
|
||||
"KEY-1", "taskNo=30001|taskType=1|"
|
||||
+ "route=070501>070502|crossFloorTarget=dirty", 9L));
|
||||
Ft546SrgvCommandIntent stage = mapper.from(envelope("CMD-2",
|
||||
"KEY-2", "taskNo=30001|taskType=9|stage=2|"
|
||||
+ "route=070501>070502|crossFloorTarget=dirty", 9L));
|
||||
|
||||
assertNull(ordinary.getCrossFloorTarget());
|
||||
assertNull(stage.getCrossFloorTarget());
|
||||
}
|
||||
|
||||
@Test
|
||||
void malformedDuplicateAndWrongTypeKeepLegacyRejections() {
|
||||
assertMessage("invalid command payload segment: broken",
|
||||
envelope("CMD-1", "KEY-1", "broken", 9L));
|
||||
assertMessage("duplicate command payload field: taskNo",
|
||||
envelope("CMD-1", "KEY-1", "taskNo=1|taskNo=2|"
|
||||
+ "taskType=1|route=1>2", 9L));
|
||||
assertMessage("legacy shuttle supports MOVE_ROUTE only",
|
||||
new DeviceCommandEnvelope("CMD-1", "KEY-1", "1",
|
||||
DeviceCommandType.CHARGE, "taskNo=1", 1, 9L, 17L));
|
||||
}
|
||||
|
||||
private void assertMessage(String message,
|
||||
DeviceCommandEnvelope envelope) {
|
||||
RuntimeException failure = assertThrows(RuntimeException.class,
|
||||
() -> mapper.from(envelope));
|
||||
assertEquals(message, failure.getMessage());
|
||||
}
|
||||
|
||||
private static DeviceCommandEnvelope envelope(String commandId,
|
||||
String commandKey,
|
||||
String payload,
|
||||
long topologyVersion) {
|
||||
return new DeviceCommandEnvelope(commandId, commandKey, "1",
|
||||
DeviceCommandType.MOVE_ROUTE, payload, 1,
|
||||
topologyVersion, 17L);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import com.ruoyi.common.domain.PlcConnectInfo;
|
||||
import com.ruoyi.device.srgv.domain.DeviceSrgv;
|
||||
import com.ruoyi.project.ft546.config.Ft546CapabilityEvidence;
|
||||
import com.ruoyi.project.ft546.config.Ft546CapabilityGate;
|
||||
import com.ruoyi.project.ft546.config.Ft546DeviceProfile;
|
||||
import com.ruoyi.ft546.protocol.Ft546DeviceProfile;
|
||||
import com.ruoyi.project.ft546.device.Ft546DeviceRuntimeRegistry;
|
||||
import com.ruoyi.project.ft546.device.Ft546MotionCommandGuard;
|
||||
import com.ruoyi.schedule.plcRead.srgv.PlcSrgvRead;
|
||||
@@ -108,8 +108,8 @@ class Ft546LegacySrgvAdapterTest {
|
||||
JsonNode fixture = fixture("srgv-point-to-point.json");
|
||||
FakePlcSrgvRead runtime = connectedRuntime("1");
|
||||
Ft546DeviceProfile profile = pointProfile();
|
||||
profile.getRegisters().put("crossFloorFlag", "4020");
|
||||
profile.getRegisters().put("crossFloorTarget", "4022");
|
||||
profile.putRegister("crossFloorFlag", "4020");
|
||||
profile.putRegister("crossFloorTarget", "4022");
|
||||
Ft546LegacySrgvAdapter adapter = adapter(runtime, profile,
|
||||
null, (deviceId, commandId) -> false);
|
||||
|
||||
@@ -127,7 +127,7 @@ class Ft546LegacySrgvAdapterTest {
|
||||
Ft546DeviceProfile profile = pointProfile();
|
||||
Ft546LegacySrgvAdapter adapter = adapter(runtime, profile,
|
||||
null, (deviceId, commandId) -> false);
|
||||
profile.getRegisters().put("path", "9999");
|
||||
profile.putRegister("path", "9999");
|
||||
|
||||
SendReceipt receipt = adapter.send(envelope("CMD-1",
|
||||
fixture.get("payload").asText()));
|
||||
@@ -223,7 +223,7 @@ class Ft546LegacySrgvAdapterTest {
|
||||
throws Exception {
|
||||
JsonNode fixture = fixture("srgv-two-stage-gate.json");
|
||||
Ft546DeviceProfile profile = twoStageProfile();
|
||||
profile.getCapabilities().remove("POINT_TO_POINT_COMMAND");
|
||||
profile.removeCapability("POINT_TO_POINT_COMMAND");
|
||||
FakePlcSrgvRead runtime = connectedRuntime("1");
|
||||
Ft546LegacySrgvAdapter adapter = adapter(runtime, profile,
|
||||
passingEvidence(profile),
|
||||
@@ -295,19 +295,19 @@ class Ft546LegacySrgvAdapterTest {
|
||||
|
||||
private static Ft546DeviceProfile pointProfile() {
|
||||
Ft546DeviceProfile profile = baseProfile();
|
||||
profile.getCapabilities().add("POINT_TO_POINT_COMMAND");
|
||||
profile.addCapability("POINT_TO_POINT_COMMAND");
|
||||
return profile;
|
||||
}
|
||||
|
||||
private static Ft546DeviceProfile twoStageProfile() {
|
||||
Ft546DeviceProfile profile = pointProfile();
|
||||
profile.getCapabilities().add("TWO_STAGE_ATOMIC_TASK");
|
||||
profile.addCapability("TWO_STAGE_ATOMIC_TASK");
|
||||
profile.setProtocolCorpusHash("sha256:ft546-two-stage-v1");
|
||||
profile.getRegisters().put("secondTaskNo", "4400");
|
||||
profile.getRegisters().put("secondTaskType", "4402");
|
||||
profile.getRegisters().put("secondPath", "4404");
|
||||
profile.getRegisters().put("secondStageFlag", "4422");
|
||||
profile.getRegisters().put("secondComplete", "4423");
|
||||
profile.putRegister("secondTaskNo", "4400");
|
||||
profile.putRegister("secondTaskType", "4402");
|
||||
profile.putRegister("secondPath", "4404");
|
||||
profile.putRegister("secondStageFlag", "4422");
|
||||
profile.putRegister("secondComplete", "4423");
|
||||
return profile;
|
||||
}
|
||||
|
||||
@@ -317,12 +317,12 @@ class Ft546LegacySrgvAdapterTest {
|
||||
profile.setDeviceType("SHUTTLE");
|
||||
profile.setAdapterType("FT546_LEGACY_SRGV");
|
||||
profile.setProfileVersion(1L);
|
||||
profile.getRegisters().put("taskNo", "4000");
|
||||
profile.getRegisters().put("taskType", "4002");
|
||||
profile.getRegisters().put("path", "4004");
|
||||
profile.getRegisters().put("trigger", "4021");
|
||||
profile.getRegisters().put("status", "4068");
|
||||
profile.getRegisters().put("execution", "4060");
|
||||
profile.putRegister("taskNo", "4000");
|
||||
profile.putRegister("taskType", "4002");
|
||||
profile.putRegister("path", "4004");
|
||||
profile.putRegister("trigger", "4021");
|
||||
profile.putRegister("status", "4068");
|
||||
profile.putRegister("execution", "4060");
|
||||
return profile;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user