feat(ft546): add WCS adapter configuration profiles
This commit is contained in:
@@ -0,0 +1,28 @@
|
|||||||
|
package com.ruoyi.project.ft546.config;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public final class CapabilityGateResult {
|
||||||
|
private final boolean publishable;
|
||||||
|
private final String reasonCode;
|
||||||
|
private final Set<String> enabledCapabilities;
|
||||||
|
|
||||||
|
CapabilityGateResult(boolean publishable, String reasonCode,
|
||||||
|
Set<String> enabledCapabilities) {
|
||||||
|
this.publishable = publishable;
|
||||||
|
this.reasonCode = reasonCode;
|
||||||
|
this.enabledCapabilities = Collections.unmodifiableSet(
|
||||||
|
new LinkedHashSet<String>(enabledCapabilities));
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPublishable() { return publishable; }
|
||||||
|
public String getReasonCode() { return reasonCode; }
|
||||||
|
public boolean isEnabled(String capability) {
|
||||||
|
return enabledCapabilities.contains(capability);
|
||||||
|
}
|
||||||
|
public Set<String> getEnabledCapabilities() {
|
||||||
|
return enabledCapabilities;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package com.ruoyi.project.ft546.config;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public final class Ft546CapabilityEvidence {
|
||||||
|
private final long profileVersion;
|
||||||
|
private final String corpusManifestHash;
|
||||||
|
private final String passingTestRunId;
|
||||||
|
private final String gitCommit;
|
||||||
|
private final boolean protocolRegressionPassed;
|
||||||
|
private final Instant approvedAt;
|
||||||
|
private final Instant expiresAt;
|
||||||
|
|
||||||
|
public Ft546CapabilityEvidence(long profileVersion,
|
||||||
|
String corpusManifestHash,
|
||||||
|
String passingTestRunId,
|
||||||
|
String gitCommit,
|
||||||
|
boolean protocolRegressionPassed,
|
||||||
|
Instant approvedAt, Instant expiresAt) {
|
||||||
|
if (profileVersion <= 0L) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"profileVersion must be positive");
|
||||||
|
}
|
||||||
|
this.profileVersion = profileVersion;
|
||||||
|
this.corpusManifestHash = requireText(corpusManifestHash,
|
||||||
|
"corpusManifestHash");
|
||||||
|
this.passingTestRunId = requireText(passingTestRunId,
|
||||||
|
"passingTestRunId");
|
||||||
|
this.gitCommit = requireText(gitCommit, "gitCommit");
|
||||||
|
this.protocolRegressionPassed = protocolRegressionPassed;
|
||||||
|
this.approvedAt = Objects.requireNonNull(approvedAt, "approvedAt");
|
||||||
|
this.expiresAt = Objects.requireNonNull(expiresAt, "expiresAt");
|
||||||
|
if (!expiresAt.isAfter(approvedAt)) {
|
||||||
|
throw new IllegalArgumentException("evidence expiry is invalid");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getProfileVersion() { return profileVersion; }
|
||||||
|
public String getCorpusManifestHash() { return corpusManifestHash; }
|
||||||
|
public String getPassingTestRunId() { return passingTestRunId; }
|
||||||
|
public String getGitCommit() { return gitCommit; }
|
||||||
|
public boolean isProtocolRegressionPassed() {
|
||||||
|
return protocolRegressionPassed;
|
||||||
|
}
|
||||||
|
public Instant getApprovedAt() { return approvedAt; }
|
||||||
|
public Instant getExpiresAt() { return expiresAt; }
|
||||||
|
|
||||||
|
private static String requireText(String value, String field) {
|
||||||
|
if (value == null || value.trim().isEmpty()) {
|
||||||
|
throw new IllegalArgumentException(field + " must not be blank");
|
||||||
|
}
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package com.ruoyi.project.ft546.config;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
public final class Ft546CapabilityGate {
|
||||||
|
private final Supplier<Instant> clock;
|
||||||
|
private final Ft546ConfigurationValidator validator =
|
||||||
|
new Ft546ConfigurationValidator();
|
||||||
|
|
||||||
|
public Ft546CapabilityGate(Supplier<Instant> clock) {
|
||||||
|
this.clock = Objects.requireNonNull(clock, "clock");
|
||||||
|
}
|
||||||
|
|
||||||
|
public CapabilityGateResult evaluate(Ft546DeviceProfile profile,
|
||||||
|
Ft546CapabilityEvidence evidence) {
|
||||||
|
validator.validate(profile);
|
||||||
|
Set<String> enabled = new LinkedHashSet<String>(
|
||||||
|
profile.getCapabilities());
|
||||||
|
enabled.remove("TWO_STAGE_ATOMIC_TASK");
|
||||||
|
if (!profile.getCapabilities().contains("TWO_STAGE_ATOMIC_TASK")) {
|
||||||
|
return new CapabilityGateResult(true, "PUBLISHABLE", enabled);
|
||||||
|
}
|
||||||
|
if (!matches(profile, evidence, now())) {
|
||||||
|
return new CapabilityGateResult(false,
|
||||||
|
"TWO_STAGE_EVIDENCE_REQUIRED", enabled);
|
||||||
|
}
|
||||||
|
enabled.add("TWO_STAGE_ATOMIC_TASK");
|
||||||
|
return new CapabilityGateResult(true, "PUBLISHABLE", enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean matches(Ft546DeviceProfile profile,
|
||||||
|
Ft546CapabilityEvidence evidence,
|
||||||
|
Instant now) {
|
||||||
|
return evidence != null
|
||||||
|
&& evidence.isProtocolRegressionPassed()
|
||||||
|
&& evidence.getProfileVersion() == profile.getProfileVersion()
|
||||||
|
&& evidence.getCorpusManifestHash().equals(
|
||||||
|
profile.getProtocolCorpusHash())
|
||||||
|
&& !evidence.getApprovedAt().isAfter(now)
|
||||||
|
&& evidence.getExpiresAt().isAfter(now);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Instant now() {
|
||||||
|
return Objects.requireNonNull(clock.get(), "clock value");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package com.ruoyi.project.ft546.config;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public final class Ft546ConfigurationValidator {
|
||||||
|
private static final Set<String> LEGACY_FORBIDDEN = new HashSet<String>(
|
||||||
|
Arrays.asList("COMMAND_RESULT_QUERY", "ORDERED_DEVICE_SNAPSHOT",
|
||||||
|
"CANDIDATE_BUFFER", "PREPARE_COMMIT_SEGMENT",
|
||||||
|
"IDEMPOTENT_COMMAND_KEY", "BOOT_SESSION_ID"));
|
||||||
|
private static final Set<String> LEGACY_REQUIRED_REGISTERS =
|
||||||
|
new LinkedHashSet<String>(Arrays.asList("taskNo", "taskType",
|
||||||
|
"path", "trigger", "status", "execution"));
|
||||||
|
|
||||||
|
public void validate(Ft546WcsProperties properties) {
|
||||||
|
if (properties == null || properties.getRuntimeMode() == null) {
|
||||||
|
throw new IllegalArgumentException("runtimeMode must be configured");
|
||||||
|
}
|
||||||
|
if (properties.getDispatchIntervalMs() <= 0L
|
||||||
|
|| properties.getCommandPollIntervalMs() <= 0L) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"runtime intervals must be positive");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void validate(Ft546DeviceProfile profile) {
|
||||||
|
if (profile == null) {
|
||||||
|
throw new IllegalArgumentException("profile must not be null");
|
||||||
|
}
|
||||||
|
requireText(profile.getDeviceId(), "deviceId");
|
||||||
|
requireText(profile.getDeviceType(), "deviceType");
|
||||||
|
String adapterType = requireText(profile.getAdapterType(),
|
||||||
|
"adapterType");
|
||||||
|
if (profile.getProfileVersion() <= 0L) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"profileVersion must be positive");
|
||||||
|
}
|
||||||
|
if (profile.getPort() != null
|
||||||
|
&& (profile.getPort() <= 0 || profile.getPort() > 65535)) {
|
||||||
|
throw new IllegalArgumentException("port is invalid");
|
||||||
|
}
|
||||||
|
if (profile.getStation() != null && profile.getStation() < 0) {
|
||||||
|
throw new IllegalArgumentException("station is invalid");
|
||||||
|
}
|
||||||
|
validateUniqueRegisterOwnership(profile.getRegisters());
|
||||||
|
if ("FT546_LEGACY_SRGV".equals(adapterType)) {
|
||||||
|
if (!profile.getRegisters().keySet().containsAll(
|
||||||
|
LEGACY_REQUIRED_REGISTERS)) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"legacy shuttle profile is missing required registers");
|
||||||
|
}
|
||||||
|
for (String forbidden : LEGACY_FORBIDDEN) {
|
||||||
|
if (profile.getCapabilities().contains(forbidden)) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"legacy adapter cannot claim " + forbidden);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (profile.getCapabilities().contains("TWO_STAGE_ATOMIC_TASK")) {
|
||||||
|
requireText(profile.getProtocolCorpusHash(),
|
||||||
|
"protocolCorpusHash");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateUniqueRegisterOwnership(
|
||||||
|
Map<String, String> registers) {
|
||||||
|
if (registers == null || registers.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("registers must not be empty");
|
||||||
|
}
|
||||||
|
Set<String> addresses = new HashSet<String>();
|
||||||
|
for (Map.Entry<String, String> register : registers.entrySet()) {
|
||||||
|
requireText(register.getKey(), "register name");
|
||||||
|
String address = requireText(register.getValue(),
|
||||||
|
"register address");
|
||||||
|
if (!addresses.add(address)) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"duplicate register ownership: " + address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String requireText(String value, String field) {
|
||||||
|
if (value == null || value.trim().isEmpty()) {
|
||||||
|
throw new IllegalArgumentException(field + " must not be blank");
|
||||||
|
}
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package com.ruoyi.project.ft546.config;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public class Ft546DeviceProfile {
|
||||||
|
private String deviceId;
|
||||||
|
private String deviceType;
|
||||||
|
private String adapterType;
|
||||||
|
private String ip;
|
||||||
|
private Integer port;
|
||||||
|
private Integer station;
|
||||||
|
private Set<String> capabilities = new LinkedHashSet<String>();
|
||||||
|
private Map<String, String> registers =
|
||||||
|
new LinkedHashMap<String, String>();
|
||||||
|
private long profileVersion;
|
||||||
|
private String protocolCorpusHash;
|
||||||
|
|
||||||
|
public String getDeviceId() { return deviceId; }
|
||||||
|
public void setDeviceId(String deviceId) { this.deviceId = deviceId; }
|
||||||
|
public String getDeviceType() { return deviceType; }
|
||||||
|
public void setDeviceType(String deviceType) { this.deviceType = deviceType; }
|
||||||
|
public String getAdapterType() { return adapterType; }
|
||||||
|
public void setAdapterType(String adapterType) { this.adapterType = adapterType; }
|
||||||
|
public String getIp() { return ip; }
|
||||||
|
public void setIp(String ip) { this.ip = ip; }
|
||||||
|
public Integer getPort() { return port; }
|
||||||
|
public void setPort(Integer port) { this.port = port; }
|
||||||
|
public Integer getStation() { return station; }
|
||||||
|
public void setStation(Integer station) { this.station = station; }
|
||||||
|
public Set<String> getCapabilities() { return capabilities; }
|
||||||
|
public void setCapabilities(Set<String> capabilities) {
|
||||||
|
this.capabilities = capabilities == null
|
||||||
|
? new LinkedHashSet<String>()
|
||||||
|
: new LinkedHashSet<String>(capabilities);
|
||||||
|
}
|
||||||
|
public Map<String, String> getRegisters() { return registers; }
|
||||||
|
public void setRegisters(Map<String, String> registers) {
|
||||||
|
this.registers = registers == null
|
||||||
|
? new LinkedHashMap<String, String>()
|
||||||
|
: new LinkedHashMap<String, String>(registers);
|
||||||
|
}
|
||||||
|
public long getProfileVersion() { return profileVersion; }
|
||||||
|
public void setProfileVersion(long profileVersion) {
|
||||||
|
this.profileVersion = profileVersion;
|
||||||
|
}
|
||||||
|
public String getProtocolCorpusHash() { return protocolCorpusHash; }
|
||||||
|
public void setProtocolCorpusHash(String protocolCorpusHash) {
|
||||||
|
this.protocolCorpusHash = protocolCorpusHash;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.ruoyi.project.ft546.config;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@ConfigurationProperties(prefix = "ft546.wcs")
|
||||||
|
public class Ft546WcsProperties {
|
||||||
|
public enum RuntimeMode {
|
||||||
|
LEGACY,
|
||||||
|
SHADOW,
|
||||||
|
STANDARD
|
||||||
|
}
|
||||||
|
|
||||||
|
private RuntimeMode runtimeMode = RuntimeMode.LEGACY;
|
||||||
|
private long dispatchIntervalMs = 1000L;
|
||||||
|
private long commandPollIntervalMs = 200L;
|
||||||
|
|
||||||
|
public RuntimeMode getRuntimeMode() { return runtimeMode; }
|
||||||
|
public void setRuntimeMode(RuntimeMode runtimeMode) {
|
||||||
|
this.runtimeMode = runtimeMode;
|
||||||
|
}
|
||||||
|
public long getDispatchIntervalMs() { return dispatchIntervalMs; }
|
||||||
|
public void setDispatchIntervalMs(long dispatchIntervalMs) {
|
||||||
|
this.dispatchIntervalMs = dispatchIntervalMs;
|
||||||
|
}
|
||||||
|
public long getCommandPollIntervalMs() { return commandPollIntervalMs; }
|
||||||
|
public void setCommandPollIntervalMs(long commandPollIntervalMs) {
|
||||||
|
this.commandPollIntervalMs = commandPollIntervalMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
ruoyi-admin/src/main/resources/ft546/wcs-device-profiles.yml
Normal file
15
ruoyi-admin/src/main/resources/ft546/wcs-device-profiles.yml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
devices:
|
||||||
|
- deviceId: "1"
|
||||||
|
deviceType: SHUTTLE
|
||||||
|
adapterType: FT546_LEGACY_SRGV
|
||||||
|
profileVersion: 1
|
||||||
|
protocolCorpusHash: "UNAPPROVED_TWO_STAGE_CORPUS"
|
||||||
|
capabilities:
|
||||||
|
- POINT_TO_POINT_COMMAND
|
||||||
|
registers:
|
||||||
|
taskNo: "4000"
|
||||||
|
taskType: "4002"
|
||||||
|
path: "4004"
|
||||||
|
trigger: "4021"
|
||||||
|
status: "4068"
|
||||||
|
execution: "4060"
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
package com.ruoyi.project.ft546.config;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
class Ft546ConfigurationValidatorTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void defaultsToLegacySoDeploymentDoesNotTakeSchedulingOwnership() {
|
||||||
|
Ft546WcsProperties properties = new Ft546WcsProperties();
|
||||||
|
|
||||||
|
assertEquals(Ft546WcsProperties.RuntimeMode.LEGACY,
|
||||||
|
properties.getRuntimeMode());
|
||||||
|
assertTrue(properties.getDispatchIntervalMs() > 0L);
|
||||||
|
assertTrue(properties.getCommandPollIntervalMs() > 0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsLegacyProfileThatClaimsPrepareCommit() {
|
||||||
|
Ft546DeviceProfile profile = legacySrgvProfile();
|
||||||
|
profile.getCapabilities().add("PREPARE_COMMIT_SEGMENT");
|
||||||
|
|
||||||
|
assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> new Ft546ConfigurationValidator().validate(profile));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsDuplicateRegisterOwnershipWithinOneDevice() {
|
||||||
|
Ft546DeviceProfile profile = legacySrgvProfile();
|
||||||
|
profile.getRegisters().put("taskType", "4000");
|
||||||
|
|
||||||
|
assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> new Ft546ConfigurationValidator().validate(profile));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsTwoStageCapabilityWithoutPassingEvidenceForExactProfile() {
|
||||||
|
Ft546DeviceProfile profile = legacySrgvProfile();
|
||||||
|
profile.getCapabilities().add("TWO_STAGE_ATOMIC_TASK");
|
||||||
|
Ft546CapabilityGate gate = gate();
|
||||||
|
|
||||||
|
CapabilityGateResult result = gate.evaluate(profile, null);
|
||||||
|
|
||||||
|
assertFalse(result.isPublishable());
|
||||||
|
assertEquals("TWO_STAGE_EVIDENCE_REQUIRED", result.getReasonCode());
|
||||||
|
assertFalse(result.isEnabled("TWO_STAGE_ATOMIC_TASK"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void enablesTwoStageOnlyForPassingUnexpiredEvidenceBoundToProfile() {
|
||||||
|
Ft546DeviceProfile profile = legacySrgvProfile();
|
||||||
|
profile.getCapabilities().add("TWO_STAGE_ATOMIC_TASK");
|
||||||
|
Ft546CapabilityEvidence evidence = new Ft546CapabilityEvidence(
|
||||||
|
1L, "CORPUS-SHA256", "RUN-1", "COMMIT-1", true,
|
||||||
|
Instant.parse("2026-08-10T00:00:00Z"),
|
||||||
|
Instant.parse("2026-09-10T00:00:00Z"));
|
||||||
|
|
||||||
|
CapabilityGateResult result = gate().evaluate(profile, evidence);
|
||||||
|
|
||||||
|
assertTrue(result.isPublishable());
|
||||||
|
assertTrue(result.isEnabled("TWO_STAGE_ATOMIC_TASK"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void legacyProfileKeepsUnsupportedCapabilitiesDisabled() {
|
||||||
|
CapabilityGateResult result = gate().evaluate(legacySrgvProfile(), null);
|
||||||
|
|
||||||
|
assertFalse(result.isEnabled("COMMAND_RESULT_QUERY"));
|
||||||
|
assertFalse(result.isEnabled("ORDERED_DEVICE_SNAPSHOT"));
|
||||||
|
assertFalse(result.isEnabled("CANDIDATE_BUFFER"));
|
||||||
|
assertFalse(result.isEnabled("PREPARE_COMMIT_SEGMENT"));
|
||||||
|
assertFalse(result.isEnabled("IDEMPOTENT_COMMAND_KEY"));
|
||||||
|
assertFalse(result.isEnabled("BOOT_SESSION_ID"));
|
||||||
|
assertTrue(result.isEnabled("POINT_TO_POINT_COMMAND"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void checkedInProfileKeepsEndpointsOutAndLegacyAddressesExact()
|
||||||
|
throws Exception {
|
||||||
|
String yaml = readProjectFile("ruoyi-admin/src/main/resources/ft546/"
|
||||||
|
+ "wcs-device-profiles.yml");
|
||||||
|
|
||||||
|
assertFalse(yaml.contains("192.168."));
|
||||||
|
assertFalse(yaml.toLowerCase().contains("password"));
|
||||||
|
assertTrue(yaml.contains("taskNo: \"4000\""));
|
||||||
|
assertTrue(yaml.contains("taskType: \"4002\""));
|
||||||
|
assertTrue(yaml.contains("path: \"4004\""));
|
||||||
|
assertTrue(yaml.contains("trigger: \"4021\""));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void packagesAdminOwnedAdapterConfigurationMigration() throws Exception {
|
||||||
|
String sql = readProjectFile(
|
||||||
|
"sql/wcs/V004__ft546_project_adapter_config.sql");
|
||||||
|
|
||||||
|
assertTrue(sql.contains("project_device_endpoint"));
|
||||||
|
assertTrue(sql.contains("project_plc_address"));
|
||||||
|
assertTrue(sql.contains("project_protocol_profile"));
|
||||||
|
assertTrue(sql.contains("project_protocol_capability_evidence"));
|
||||||
|
assertTrue(sql.contains("project_wms_mapping"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Ft546CapabilityGate gate() {
|
||||||
|
return new Ft546CapabilityGate(() ->
|
||||||
|
Instant.parse("2026-08-11T00:00:00Z"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Ft546DeviceProfile legacySrgvProfile() {
|
||||||
|
Ft546DeviceProfile profile = new Ft546DeviceProfile();
|
||||||
|
profile.setDeviceId("1");
|
||||||
|
profile.setDeviceType("SHUTTLE");
|
||||||
|
profile.setAdapterType("FT546_LEGACY_SRGV");
|
||||||
|
profile.setProfileVersion(1L);
|
||||||
|
profile.setProtocolCorpusHash("CORPUS-SHA256");
|
||||||
|
profile.setCapabilities(new LinkedHashSet<String>(
|
||||||
|
Collections.singletonList("POINT_TO_POINT_COMMAND")));
|
||||||
|
LinkedHashMap<String, String> registers = new LinkedHashMap<String, String>();
|
||||||
|
registers.put("taskNo", "4000");
|
||||||
|
registers.put("taskType", "4002");
|
||||||
|
registers.put("path", "4004");
|
||||||
|
registers.put("trigger", "4021");
|
||||||
|
registers.put("status", "4068");
|
||||||
|
registers.put("execution", "4060");
|
||||||
|
profile.setRegisters(registers);
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String readProjectFile(String relative) throws Exception {
|
||||||
|
Path current = Paths.get(System.getProperty(
|
||||||
|
"maven.multiModuleProjectDirectory", "."))
|
||||||
|
.toAbsolutePath().normalize();
|
||||||
|
while (current != null) {
|
||||||
|
Path file = current.resolve(relative);
|
||||||
|
if (Files.isRegularFile(file)) {
|
||||||
|
return new String(Files.readAllBytes(file),
|
||||||
|
StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
current = current.getParent();
|
||||||
|
}
|
||||||
|
throw new java.nio.file.NoSuchFileException(relative);
|
||||||
|
}
|
||||||
|
}
|
||||||
127
sql/wcs/V004__ft546_project_adapter_config.sql
Normal file
127
sql/wcs/V004__ft546_project_adapter_config.sql
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
SET XACT_ABORT ON;
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
IF OBJECT_ID('dbo.project_device_endpoint', 'U') IS NULL
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE dbo.project_device_endpoint (
|
||||||
|
device_id varchar(64) NOT NULL PRIMARY KEY,
|
||||||
|
device_type varchar(32) NOT NULL,
|
||||||
|
adapter_type varchar(64) NOT NULL,
|
||||||
|
endpoint_ip varchar(128) NOT NULL,
|
||||||
|
endpoint_port int NOT NULL,
|
||||||
|
protocol_station int NULL,
|
||||||
|
enabled_flag bit NOT NULL
|
||||||
|
CONSTRAINT df_project_device_endpoint_enabled DEFAULT (0),
|
||||||
|
state_version bigint NOT NULL,
|
||||||
|
updated_time datetime2(3) NOT NULL,
|
||||||
|
CONSTRAINT ck_project_device_endpoint_port CHECK (
|
||||||
|
endpoint_port > 0 AND endpoint_port <= 65535),
|
||||||
|
CONSTRAINT ck_project_device_endpoint_station CHECK (
|
||||||
|
protocol_station IS NULL OR protocol_station >= 0),
|
||||||
|
CONSTRAINT ck_project_device_endpoint_version CHECK (state_version >= 0)
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF OBJECT_ID('dbo.project_protocol_profile', 'U') IS NULL
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE dbo.project_protocol_profile (
|
||||||
|
device_id varchar(64) NOT NULL,
|
||||||
|
profile_version bigint NOT NULL,
|
||||||
|
device_type varchar(32) NOT NULL,
|
||||||
|
adapter_type varchar(64) NOT NULL,
|
||||||
|
corpus_manifest_hash varchar(128) NOT NULL,
|
||||||
|
profile_json nvarchar(max) NOT NULL,
|
||||||
|
active_flag bit NOT NULL
|
||||||
|
CONSTRAINT df_project_protocol_profile_active DEFAULT (0),
|
||||||
|
created_time datetime2(3) NOT NULL,
|
||||||
|
activated_time datetime2(3) NULL,
|
||||||
|
CONSTRAINT pk_project_protocol_profile PRIMARY KEY (
|
||||||
|
device_id, profile_version),
|
||||||
|
CONSTRAINT fk_project_protocol_profile_endpoint FOREIGN KEY (device_id)
|
||||||
|
REFERENCES dbo.project_device_endpoint(device_id),
|
||||||
|
CONSTRAINT ck_project_protocol_profile_version CHECK (
|
||||||
|
profile_version > 0)
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM sys.indexes
|
||||||
|
WHERE object_id = OBJECT_ID('dbo.project_protocol_profile')
|
||||||
|
AND name = 'uk_project_protocol_profile_active')
|
||||||
|
BEGIN
|
||||||
|
CREATE UNIQUE INDEX uk_project_protocol_profile_active
|
||||||
|
ON dbo.project_protocol_profile(device_id)
|
||||||
|
WHERE active_flag = 1;
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF OBJECT_ID('dbo.project_plc_address', 'U') IS NULL
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE dbo.project_plc_address (
|
||||||
|
device_id varchar(64) NOT NULL,
|
||||||
|
profile_version bigint NOT NULL,
|
||||||
|
register_name varchar(64) NOT NULL,
|
||||||
|
plc_address varchar(64) NOT NULL,
|
||||||
|
value_type varchar(32) NOT NULL,
|
||||||
|
semantic_code varchar(128) NOT NULL,
|
||||||
|
write_order int NULL,
|
||||||
|
CONSTRAINT pk_project_plc_address PRIMARY KEY (
|
||||||
|
device_id, profile_version, register_name),
|
||||||
|
CONSTRAINT fk_project_plc_address_profile FOREIGN KEY (
|
||||||
|
device_id, profile_version)
|
||||||
|
REFERENCES dbo.project_protocol_profile(
|
||||||
|
device_id, profile_version),
|
||||||
|
CONSTRAINT uk_project_plc_address_owner UNIQUE (
|
||||||
|
device_id, profile_version, plc_address),
|
||||||
|
CONSTRAINT ck_project_plc_address_order CHECK (
|
||||||
|
write_order IS NULL OR write_order > 0)
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF OBJECT_ID('dbo.project_protocol_capability_evidence', 'U') IS NULL
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE dbo.project_protocol_capability_evidence (
|
||||||
|
evidence_id varchar(128) NOT NULL PRIMARY KEY,
|
||||||
|
device_id varchar(64) NOT NULL,
|
||||||
|
profile_version bigint NOT NULL,
|
||||||
|
capability_code varchar(64) NOT NULL,
|
||||||
|
corpus_manifest_hash varchar(128) NOT NULL,
|
||||||
|
passing_test_run_id varchar(128) NOT NULL,
|
||||||
|
git_commit varchar(128) NOT NULL,
|
||||||
|
regression_passed_flag bit NOT NULL,
|
||||||
|
approved_by varchar(128) NOT NULL,
|
||||||
|
approved_time datetime2(3) NOT NULL,
|
||||||
|
expires_time datetime2(3) NOT NULL,
|
||||||
|
created_time datetime2(3) NOT NULL,
|
||||||
|
CONSTRAINT fk_project_capability_evidence_profile FOREIGN KEY (
|
||||||
|
device_id, profile_version)
|
||||||
|
REFERENCES dbo.project_protocol_profile(
|
||||||
|
device_id, profile_version),
|
||||||
|
CONSTRAINT uk_project_capability_evidence_binding UNIQUE (
|
||||||
|
device_id, profile_version, capability_code,
|
||||||
|
corpus_manifest_hash, passing_test_run_id),
|
||||||
|
CONSTRAINT ck_project_capability_evidence_expiry CHECK (
|
||||||
|
expires_time > approved_time)
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF OBJECT_ID('dbo.project_wms_mapping', 'U') IS NULL
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE dbo.project_wms_mapping (
|
||||||
|
mapping_id varchar(128) NOT NULL PRIMARY KEY,
|
||||||
|
external_operation varchar(64) NOT NULL,
|
||||||
|
external_order_type varchar(64) NULL,
|
||||||
|
canonical_order_type varchar(32) NOT NULL,
|
||||||
|
source_field varchar(128) NOT NULL,
|
||||||
|
destination_field varchar(128) NOT NULL,
|
||||||
|
callback_url_config_key varchar(128) NULL,
|
||||||
|
active_flag bit NOT NULL
|
||||||
|
CONSTRAINT df_project_wms_mapping_active DEFAULT (0),
|
||||||
|
state_version bigint NOT NULL,
|
||||||
|
updated_time datetime2(3) NOT NULL,
|
||||||
|
CONSTRAINT uk_project_wms_mapping_operation UNIQUE (
|
||||||
|
external_operation, external_order_type),
|
||||||
|
CONSTRAINT ck_project_wms_mapping_version CHECK (state_version >= 0)
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
|
||||||
|
COMMIT TRANSACTION;
|
||||||
Reference in New Issue
Block a user