feat(ft546): adapt lift conveyor and charger protocols

This commit is contained in:
zgw
2026-08-11 13:23:53 +08:00
parent ff384cc019
commit 14a5a6174e
15 changed files with 907 additions and 36 deletions

View File

@@ -0,0 +1,164 @@
package com.ruoyi.project.ft546.device;
import com.ruoyi.project.ft546.config.Ft546ConfigurationValidator;
import com.ruoyi.project.ft546.config.Ft546DeviceProfile;
import com.ruoyi.wcs.application.device.DeviceCommandEnvelope;
import com.ruoyi.wcs.application.device.SendReceipt;
import com.ruoyi.wcs.domain.execution.DeviceCommandType;
import com.ruoyi.wcs.domain.execution.DeviceSnapshot;
import com.ruoyi.wcs.port.out.DeviceCommandGateway;
import com.ruoyi.wcs.port.out.DeviceSnapshotGateway;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public abstract class AbstractFt546EquipmentAdapter
implements DeviceCommandGateway, DeviceSnapshotGateway {
private final Ft546DeviceProfile profile;
private final DeviceCommandType commandType;
private final Ft546MotionCommandGuard motionCommandGuard;
private final Ft546SnapshotContextProvider contextProvider;
private final Set<String> locallyUnknownDevices =
Collections.newSetFromMap(
new ConcurrentHashMap<String, Boolean>());
protected AbstractFt546EquipmentAdapter(
Ft546DeviceProfile sourceProfile, String expectedDeviceType,
String requiredCapability, DeviceCommandType commandType,
Ft546MotionCommandGuard motionCommandGuard,
Ft546SnapshotContextProvider contextProvider) {
this.profile = copy(Objects.requireNonNull(sourceProfile,
"profile"));
new Ft546ConfigurationValidator().validate(profile);
if (!expectedDeviceType.equals(profile.getDeviceType())) {
throw new IllegalArgumentException(
"profile device type must be " + expectedDeviceType);
}
if (!profile.getCapabilities().contains(requiredCapability)) {
throw new IllegalStateException(
requiredCapability + " is not enabled");
}
commandAddress();
this.commandType = Objects.requireNonNull(commandType, "commandType");
this.motionCommandGuard = Objects.requireNonNull(motionCommandGuard,
"motionCommandGuard");
this.contextProvider = Objects.requireNonNull(contextProvider,
"contextProvider");
}
@Override
public final SendReceipt send(DeviceCommandEnvelope envelope) {
Objects.requireNonNull(envelope, "envelope");
String receiptId = receiptId(envelope);
if (!profile.getDeviceId().equals(envelope.getDeviceId())) {
return SendReceipt.rejected(receiptId,
"profile belongs to another device");
}
if (envelope.getType() != commandType) {
return SendReceipt.rejected(receiptId,
"command type is not supported by this adapter");
}
if (locallyUnknownDevices.contains(envelope.getDeviceId())
|| motionCommandGuard.hasBlockingMotionCommand(
envelope.getDeviceId(), envelope.getCommandId())) {
return SendReceipt.rejected(receiptId,
"device has another active command or unresolved UNKNOWN");
}
final byte[] payload;
try {
payload = encode(envelope.getPayload());
} catch (IllegalArgumentException rejected) {
return SendReceipt.rejected(receiptId, rejected.getMessage());
}
if (!isConnected()) {
return SendReceipt.notSent("equipment PLC is disconnected");
}
try {
int result = write(commandAddress(), payload);
if (result <= 0) {
return markUnknown(envelope.getDeviceId(), receiptId,
"equipment write result is ambiguous");
}
return SendReceipt.accepted(receiptId);
} catch (RuntimeException ambiguous) {
return markUnknown(envelope.getDeviceId(), receiptId,
"equipment write raised "
+ ambiguous.getClass().getSimpleName());
}
}
@Override
public final DeviceSnapshot readSnapshot(String deviceId) {
if (!profile.getDeviceId().equals(deviceId)) {
throw new IllegalArgumentException(
"profile belongs to another device");
}
return snapshot(deviceId);
}
protected abstract boolean isConnected();
protected abstract int write(String address, byte[] payload);
protected abstract byte[] encode(String payload);
protected abstract DeviceSnapshot snapshot(String deviceId);
protected final DeviceSnapshot unorderedSnapshot(
String deviceId, String taskNumber, String currentNode,
Map<String, String> facts) {
Ft546SnapshotContext context = Objects.requireNonNull(
contextProvider.contextFor(deviceId), "snapshot context");
return DeviceSnapshot.unorderedWithoutBootSession(
context.getSnapshotId(), deviceId, context.getCommandKey(),
fact(taskNumber), context.getObservedAt(), isConnected(),
context.getTopologyVersion(), context.getFencingToken(),
fact(currentNode), Collections.singletonList(fact(currentNode)),
false, new LinkedHashMap<String, String>(facts));
}
protected final String commandAddress() {
String address = profile.getRegisters().get("command");
if (address == null || address.trim().isEmpty()) {
throw new IllegalArgumentException(
"profile register is missing: command");
}
return address.trim();
}
protected static String value(Object value) {
return value == null ? "UNKNOWN" : String.valueOf(value);
}
protected static String fact(String value) {
return value == null || value.trim().isEmpty()
? "UNKNOWN" : value.trim();
}
private SendReceipt markUnknown(String deviceId, String receiptId,
String detail) {
locallyUnknownDevices.add(deviceId);
return SendReceipt.acceptedUnknown(receiptId, detail);
}
private static String receiptId(DeviceCommandEnvelope envelope) {
return "FT546:" + envelope.getCommandId() + ":"
+ envelope.getAttempt();
}
private static Ft546DeviceProfile copy(Ft546DeviceProfile source) {
Ft546DeviceProfile copy = new Ft546DeviceProfile();
copy.setDeviceId(source.getDeviceId());
copy.setDeviceType(source.getDeviceType());
copy.setAdapterType(source.getAdapterType());
copy.setIp(source.getIp());
copy.setPort(source.getPort());
copy.setStation(source.getStation());
copy.setCapabilities(source.getCapabilities());
copy.setRegisters(source.getRegisters());
copy.setProfileVersion(source.getProfileVersion());
copy.setProtocolCorpusHash(source.getProtocolCorpusHash());
return copy;
}
}

View File

@@ -0,0 +1,98 @@
package com.ruoyi.project.ft546.device;
import java.io.ByteArrayOutputStream;
import java.util.LinkedHashMap;
import java.util.Map;
public final class Ft546LegacyPayloadCodec {
private Ft546LegacyPayloadCodec() { }
public static Map<String, String> parse(String payload) {
if (payload == null || payload.trim().isEmpty()) {
throw new IllegalArgumentException("payload must not be blank");
}
Map<String, String> values = new LinkedHashMap<String, String>();
for (String segment : payload.split("\\|")) {
int separator = segment.indexOf('=');
if (separator <= 0 || separator == segment.length() - 1) {
throw new IllegalArgumentException(
"invalid command payload segment: " + segment);
}
String key = segment.substring(0, separator).trim();
String value = segment.substring(separator + 1).trim();
if (values.put(key, value) != null) {
throw new IllegalArgumentException(
"duplicate command payload field: " + key);
}
}
return values;
}
public static int positiveInt(Map<String, String> values, String field) {
int value = integer(values, field);
if (value <= 0) {
throw new IllegalArgumentException(field + " must be positive");
}
return value;
}
public static int binaryFlag(Map<String, String> values, String field) {
int value = integer(values, field);
if (value != 0 && value != 1) {
throw new IllegalArgumentException(field + " must be 0 or 1");
}
return value;
}
public static int integer(Map<String, String> values, String field) {
String value = values.get(field);
if (value == null || value.trim().isEmpty()) {
throw new IllegalArgumentException(field + " is required");
}
try {
return Integer.parseInt(value);
} catch (NumberFormatException invalid) {
throw new IllegalArgumentException(field + " is not an integer",
invalid);
}
}
public static byte[] fields(Field... fields) {
ByteArrayOutputStream output = new ByteArrayOutputStream();
for (Field field : fields) {
for (int shift = (field.width - 1) * 8; shift >= 0; shift -= 8) {
output.write((field.value >>> shift) & 0xff);
}
}
return output.toByteArray();
}
public static int checksum(int... values) {
int sum = 0;
for (int value : values) {
sum += (value >>> 24) & 0xff;
sum += (value >>> 16) & 0xff;
sum += (value >>> 8) & 0xff;
sum += value & 0xff;
}
return sum;
}
public static Field int32(int value) { return new Field(4, value); }
public static Field int16(int value) {
if (value < Short.MIN_VALUE || value > 0xffff) {
throw new IllegalArgumentException("int16 value is out of range");
}
return new Field(2, value);
}
public static final class Field {
private final int width;
private final int value;
private Field(int width, int value) {
this.width = width;
this.value = value;
}
}
}

View File

@@ -0,0 +1,6 @@
package com.ruoyi.project.ft546.device;
@FunctionalInterface
public interface Ft546MotionCommandGuard {
boolean hasBlockingMotionCommand(String deviceId, String commandId);
}

View File

@@ -0,0 +1,39 @@
package com.ruoyi.project.ft546.device;
import java.time.Instant;
import java.util.Objects;
public final class Ft546SnapshotContext {
private final String snapshotId;
private final String commandKey;
private final long topologyVersion;
private final long fencingToken;
private final Instant observedAt;
public Ft546SnapshotContext(String snapshotId, String commandKey,
long topologyVersion, long fencingToken,
Instant observedAt) {
this.snapshotId = requireText(snapshotId, "snapshotId");
this.commandKey = requireText(commandKey, "commandKey");
if (topologyVersion <= 0L || fencingToken <= 0L) {
throw new IllegalArgumentException(
"snapshot context versions are invalid");
}
this.topologyVersion = topologyVersion;
this.fencingToken = fencingToken;
this.observedAt = Objects.requireNonNull(observedAt, "observedAt");
}
public String getSnapshotId() { return snapshotId; }
public String getCommandKey() { return commandKey; }
public long getTopologyVersion() { return topologyVersion; }
public long getFencingToken() { return fencingToken; }
public Instant getObservedAt() { return observedAt; }
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();
}
}

View File

@@ -0,0 +1,6 @@
package com.ruoyi.project.ft546.device;
@FunctionalInterface
public interface Ft546SnapshotContextProvider {
Ft546SnapshotContext contextFor(String deviceId);
}

View File

@@ -0,0 +1,70 @@
package com.ruoyi.project.ft546.device.charger;
import com.ruoyi.device.cdz.domain.DeviceCdz;
import com.ruoyi.project.ft546.config.Ft546DeviceProfile;
import com.ruoyi.project.ft546.device.AbstractFt546EquipmentAdapter;
import com.ruoyi.project.ft546.device.Ft546LegacyPayloadCodec;
import com.ruoyi.project.ft546.device.Ft546MotionCommandGuard;
import com.ruoyi.project.ft546.device.Ft546SnapshotContextProvider;
import com.ruoyi.schedule.plcRead.cdz.PlcCdzRead;
import com.ruoyi.wcs.domain.execution.DeviceCommandType;
import com.ruoyi.wcs.domain.execution.DeviceSnapshot;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
public final class Ft546ChargerAdapter
extends AbstractFt546EquipmentAdapter {
private final PlcCdzRead runtime;
public Ft546ChargerAdapter(PlcCdzRead runtime,
Ft546DeviceProfile profile,
Ft546MotionCommandGuard motionCommandGuard,
Ft546SnapshotContextProvider contextProvider) {
super(profile, "CHARGER", "CHARGE", DeviceCommandType.CHARGE,
motionCommandGuard, contextProvider);
this.runtime = Objects.requireNonNull(runtime, "runtime");
runtime.requireDevice(profile.getDeviceId());
}
@Override
protected boolean isConnected() { return runtime.IsConnect(); }
@Override
protected int write(String address, byte[] payload) {
return runtime.write(address, payload);
}
@Override
protected byte[] encode(String payload) {
Map<String, String> values = Ft546LegacyPayloadCodec.parse(payload);
int enabled = Ft546LegacyPayloadCodec.binaryFlag(values, "enabled");
return Ft546LegacyPayloadCodec.fields(
Ft546LegacyPayloadCodec.int16(enabled));
}
@Override
protected DeviceSnapshot snapshot(String deviceId) {
DeviceCdz device = runtime.requireDevice(deviceId);
Map<String, String> facts = new LinkedHashMap<String, String>();
facts.put("connection", isConnected() ? "ONLINE" : "OFFLINE");
facts.put("occupancy", isBlank(device.getSpare1())
? "FREE" : "OCCUPIED");
facts.put("occupyingVehicle", fact(device.getSpare1()));
facts.put("charging", Integer.valueOf(1).equals(device.getStatus())
? "ON" : "OFF");
facts.put("fault", "UNAVAILABLE");
facts.put("status", value(device.getStatus()));
facts.put("mode", value(device.getModel()));
facts.put("site", fact(device.getSiteId()));
facts.put("floor", value(device.getSzcs()));
facts.put("taskNumber", value(device.getRwh()));
return unorderedSnapshot(deviceId, value(device.getRwh()),
fact(device.getSiteId()), facts);
}
private static boolean isBlank(String value) {
return value == null || value.trim().isEmpty();
}
}

View File

@@ -0,0 +1,74 @@
package com.ruoyi.project.ft546.device.conveyor;
import com.ruoyi.device.ssj.domain.DeviceSsj;
import com.ruoyi.project.ft546.config.Ft546DeviceProfile;
import com.ruoyi.project.ft546.device.AbstractFt546EquipmentAdapter;
import com.ruoyi.project.ft546.device.Ft546LegacyPayloadCodec;
import com.ruoyi.project.ft546.device.Ft546MotionCommandGuard;
import com.ruoyi.project.ft546.device.Ft546SnapshotContextProvider;
import com.ruoyi.schedule.plcRead.ssj.PlcSsjRead;
import com.ruoyi.wcs.domain.execution.DeviceCommandType;
import com.ruoyi.wcs.domain.execution.DeviceSnapshot;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
public final class Ft546ConveyorAdapter
extends AbstractFt546EquipmentAdapter {
private final PlcSsjRead runtime;
public Ft546ConveyorAdapter(PlcSsjRead runtime,
Ft546DeviceProfile profile,
Ft546MotionCommandGuard motionCommandGuard,
Ft546SnapshotContextProvider contextProvider) {
super(profile, "CONVEYOR", "CONVEYOR_TRANSFER",
DeviceCommandType.CONVEYOR_TRANSFER, motionCommandGuard,
contextProvider);
this.runtime = Objects.requireNonNull(runtime, "runtime");
runtime.requireDevice(profile.getDeviceId());
}
@Override
protected boolean isConnected() { return runtime.IsConnect(); }
@Override
protected int write(String address, byte[] payload) {
return runtime.write(address, payload);
}
@Override
protected byte[] encode(String payload) {
Map<String, String> values = Ft546LegacyPayloadCodec.parse(payload);
int taskNo = Ft546LegacyPayloadCodec.positiveInt(values, "taskNo");
int destination = Ft546LegacyPayloadCodec.positiveInt(values,
"destination");
int completed = Ft546LegacyPayloadCodec.binaryFlag(values,
"completed");
int checksum = Ft546LegacyPayloadCodec.checksum(taskNo, destination,
completed);
return Ft546LegacyPayloadCodec.fields(
Ft546LegacyPayloadCodec.int32(taskNo),
Ft546LegacyPayloadCodec.int16(destination),
Ft546LegacyPayloadCodec.int16(completed),
Ft546LegacyPayloadCodec.int32(checksum));
}
@Override
protected DeviceSnapshot snapshot(String deviceId) {
DeviceSsj device = runtime.requireDevice(deviceId);
Map<String, String> facts = new LinkedHashMap<String, String>();
facts.put("connection", isConnected() ? "ONLINE" : "OFFLINE");
facts.put("goodsState", device.getDw() == 1
? "PRESENT" : "ABSENT");
facts.put("mode", value(device.getModel()));
facts.put("readyState", device.getKx() == 1
? "READY" : "BLOCKED");
facts.put("trayCode", fact(device.getTrayCode()));
facts.put("taskNumber", value(device.getTaskNo()));
facts.put("request", value(device.getRkqq()));
facts.put("faultCode", value(device.getErrorCode()));
return unorderedSnapshot(deviceId, value(device.getTaskNo()),
fact(device.getDeviceCode()), facts);
}
}

View File

@@ -0,0 +1,86 @@
package com.ruoyi.project.ft546.device.lift;
import com.ruoyi.device.tsj.domain.DeviceTsj;
import com.ruoyi.project.ft546.config.Ft546DeviceProfile;
import com.ruoyi.project.ft546.device.AbstractFt546EquipmentAdapter;
import com.ruoyi.project.ft546.device.Ft546LegacyPayloadCodec;
import com.ruoyi.project.ft546.device.Ft546MotionCommandGuard;
import com.ruoyi.project.ft546.device.Ft546SnapshotContextProvider;
import com.ruoyi.schedule.plcRead.tsj.PlcTsjRead;
import com.ruoyi.wcs.domain.execution.DeviceCommandType;
import com.ruoyi.wcs.domain.execution.DeviceSnapshot;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
public final class Ft546LiftAdapter extends AbstractFt546EquipmentAdapter {
private final PlcTsjRead runtime;
public Ft546LiftAdapter(PlcTsjRead runtime, Ft546DeviceProfile profile,
Ft546MotionCommandGuard motionCommandGuard,
Ft546SnapshotContextProvider contextProvider) {
super(profile, "LIFT", "LIFT_TRANSFER",
DeviceCommandType.LIFT_TRANSFER, motionCommandGuard,
contextProvider);
this.runtime = Objects.requireNonNull(runtime, "runtime");
runtime.requireDevice(profile.getDeviceId());
}
@Override
protected boolean isConnected() { return runtime.IsConnect(); }
@Override
protected int write(String address, byte[] payload) {
return runtime.write(address, payload);
}
@Override
protected byte[] encode(String payload) {
Map<String, String> values = Ft546LegacyPayloadCodec.parse(payload);
int taskNo = Ft546LegacyPayloadCodec.positiveInt(values, "taskNo");
int taskType = Ft546LegacyPayloadCodec.positiveInt(values,
"taskType");
int sourceFloor = Ft546LegacyPayloadCodec.positiveInt(values,
"sourceFloor");
int targetFloor = Ft546LegacyPayloadCodec.positiveInt(values,
"targetFloor");
int completed = 1;
int checksum = Ft546LegacyPayloadCodec.checksum(taskNo, taskType,
sourceFloor, targetFloor, completed);
return Ft546LegacyPayloadCodec.fields(
Ft546LegacyPayloadCodec.int32(taskNo),
Ft546LegacyPayloadCodec.int16(taskType),
Ft546LegacyPayloadCodec.int16(sourceFloor),
Ft546LegacyPayloadCodec.int16(targetFloor),
Ft546LegacyPayloadCodec.int16(completed),
Ft546LegacyPayloadCodec.int16(checksum));
}
@Override
protected DeviceSnapshot snapshot(String deviceId) {
DeviceTsj device = runtime.requireDevice(deviceId);
Map<String, String> facts = new LinkedHashMap<String, String>();
facts.put("connection", isConnected() ? "ONLINE" : "OFFLINE");
facts.put("floor", value(device.getDqcs()));
facts.put("doorState", doorState(device.getJdbz()));
facts.put("doorStateSource", "EXECUTION_PHASE_DERIVED");
facts.put("portState", Integer.valueOf(1).equals(device.getYxxf())
? "READY" : "BLOCKED");
facts.put("load", value(device.getHczt()));
facts.put("status", value(device.getSbzt()));
facts.put("mode", value(device.getCzfs()));
facts.put("executionStage", value(device.getJdbz()));
facts.put("taskNumber", value(device.getRwh()));
return unorderedSnapshot(deviceId, value(device.getRwh()),
value(device.getDqcs()), facts);
}
private static String doorState(Integer executionStage) {
if (executionStage == null) {
return "UNKNOWN";
}
return executionStage == 2 || executionStage == 4
? "OPEN" : "CLOSED";
}
}

View File

@@ -5,6 +5,7 @@ import com.ruoyi.project.ft546.config.Ft546CapabilityEvidence;
import com.ruoyi.project.ft546.config.Ft546CapabilityGate;
import com.ruoyi.project.ft546.config.Ft546DeviceProfile;
import com.ruoyi.project.ft546.device.Ft546DeviceRuntimeRegistry;
import com.ruoyi.project.ft546.device.Ft546MotionCommandGuard;
import com.ruoyi.schedule.plcRead.srgv.PlcSrgvRead;
import com.ruoyi.wcs.application.device.DeviceCommandEnvelope;
import com.ruoyi.wcs.application.device.SendReceipt;
@@ -22,7 +23,7 @@ public final class Ft546LegacySrgvAdapter
private final Ft546DeviceRuntimeRegistry registry;
private final Ft546DeviceProfile profile;
private final CapabilityGateResult capabilities;
private final MotionCommandGuard motionCommandGuard;
private final Ft546MotionCommandGuard motionCommandGuard;
private final SnapshotContextProvider snapshotContextProvider;
private final Ft546SrgvCommandEncoder encoder =
new Ft546SrgvCommandEncoder();
@@ -36,7 +37,7 @@ public final class Ft546LegacySrgvAdapter
Ft546DeviceProfile profile,
Ft546CapabilityEvidence evidence,
Ft546CapabilityGate capabilityGate,
MotionCommandGuard motionCommandGuard,
Ft546MotionCommandGuard motionCommandGuard,
SnapshotContextProvider contextProvider) {
this.registry = Objects.requireNonNull(registry, "registry");
this.profile = copyProfile(Objects.requireNonNull(profile,
@@ -148,11 +149,6 @@ public final class Ft546LegacySrgvAdapter
return copy;
}
@FunctionalInterface
public interface MotionCommandGuard {
boolean hasBlockingMotionCommand(String deviceId, String commandId);
}
@FunctionalInterface
public interface SnapshotContextProvider {
Ft546SrgvSnapshotMapper.Context contextFor(String deviceId);

View File

@@ -42,6 +42,20 @@ public class PlcCdzRead implements Runnable {
return a;
}
public int write(String address, byte[] data) {
return WriteToCdz(address, data);
}
public DeviceCdz requireDevice(String deviceId) {
DeviceCdz device = deviceCdzMap == null
? null : deviceCdzMap.get(deviceId);
if (device == null) {
throw new IllegalArgumentException(
"charger device not found: " + deviceId);
}
return device;
}
@Override
public void run() {
while (isRun) {
@@ -99,16 +113,11 @@ public class PlcCdzRead implements Runnable {
}
public void setIsConnect(boolean bflag) {
if (!success) {
success = bflag;
}
success = bflag;
}
public boolean IsConnect() {
if (success) {
return success;
}
return false;
return success;
}
void reConnect() {

View File

@@ -237,16 +237,11 @@ public class PlcSrgvRead implements Runnable {
}
public void setIsConnect(boolean bflag) {
if (!success) {
success = bflag;
}
success = bflag;
}
public boolean IsConnect() {
if (success) {
return success;
}
return false;
return success;
}
void reConnect() {

View File

@@ -47,6 +47,20 @@ public class PlcSsjRead implements Runnable {
return a;
}
public int write(String address, byte[] data) {
return WriteToSSJ(address, data);
}
public DeviceSsj requireDevice(String deviceId) {
DeviceSsj device = deviceSsjMap == null
? null : deviceSsjMap.get(deviceId);
if (device == null) {
throw new IllegalArgumentException(
"conveyor device not found: " + deviceId);
}
return device;
}
@Override
public void run() {
while (isRun) {
@@ -223,16 +237,11 @@ public class PlcSsjRead implements Runnable {
}
public void setIsConnect(boolean bflag) {
if (!success) {
success = bflag;
}
success = bflag;
}
public boolean IsConnect() {
if (success) {
return success;
}
return false;
return success;
}
void reConnect() {

View File

@@ -43,6 +43,20 @@ public class PlcTsjRead implements Runnable {
return a;
}
public int write(String address, byte[] data) {
return WriteToTSJ(address, data);
}
public DeviceTsj requireDevice(String deviceId) {
DeviceTsj device = deviceTsjMap == null
? null : deviceTsjMap.get(deviceId);
if (device == null) {
throw new IllegalArgumentException(
"lift device not found: " + deviceId);
}
return device;
}
@Override
public void run() {
while (isRun) {
@@ -107,16 +121,11 @@ public class PlcTsjRead implements Runnable {
}
public void setIsConnect(boolean bflag) {
if (!success) {
success = bflag;
}
success = bflag;
}
public boolean IsConnect() {
if (success) {
return success;
}
return false;
return success;
}
void reConnect() {

View File

@@ -0,0 +1,309 @@
package com.ruoyi.project.ft546.device;
import com.ruoyi.common.domain.PlcConnectInfo;
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.project.ft546.device.charger.Ft546ChargerAdapter;
import com.ruoyi.project.ft546.device.conveyor.Ft546ConveyorAdapter;
import com.ruoyi.project.ft546.device.lift.Ft546LiftAdapter;
import com.ruoyi.schedule.plcRead.cdz.PlcCdzRead;
import com.ruoyi.schedule.plcRead.ssj.PlcSsjRead;
import com.ruoyi.schedule.plcRead.srgv.PlcSrgvRead;
import com.ruoyi.schedule.plcRead.tsj.PlcTsjRead;
import com.ruoyi.wcs.application.device.DeviceCommandEnvelope;
import com.ruoyi.wcs.application.device.SendReceipt;
import com.ruoyi.wcs.domain.execution.DeviceCommandType;
import com.ruoyi.wcs.domain.execution.DeviceSnapshot;
import org.junit.jupiter.api.Test;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
class Ft546EquipmentAdaptersTest {
private static final Instant NOW = Instant.parse("2026-08-11T05:00:00Z");
@Test
void liftAdapterReportsFactsWithoutReleasingCoreResources() {
DeviceTsj device = liftDevice();
device.setDqcs(3);
device.setJdbz(2);
device.setHczt(1);
device.setYxxf(1);
device.setSbzt(2);
device.setCzfs(2);
device.setRwh(30001L);
FakeTsjRead runtime = new FakeTsjRead("1", device);
Ft546LiftAdapter adapter = new Ft546LiftAdapter(runtime,
profile("1", "LIFT", "LIFT_TRANSFER", "DB602.0"),
(deviceId, commandId) -> false, contextProvider());
DeviceSnapshot snapshot = adapter.readSnapshot("1");
assertEquals("3", snapshot.getFacts().get("floor"));
assertEquals("OPEN", snapshot.getFacts().get("doorState"));
assertEquals("READY", snapshot.getFacts().get("portState"));
assertEquals("1", snapshot.getFacts().get("load"));
assertEquals("2", snapshot.getFacts().get("status"));
assertEquals("2", snapshot.getFacts().get("mode"));
assertFalse(snapshot.getFacts().containsKey("releaseLiftResource"));
assertFalse(snapshot.hasOrderedDeviceSequence());
assertFalse(snapshot.hasDeviceBootSession());
assertFalse(snapshot.isCommandCompleted());
}
@Test
void liftCommandKeepsTheLegacyByteLayout() {
FakeTsjRead runtime = new FakeTsjRead("1", liftDevice());
Ft546LiftAdapter adapter = new Ft546LiftAdapter(runtime,
profile("1", "LIFT", "LIFT_TRANSFER", "DB602.0"),
(deviceId, commandId) -> false, contextProvider());
SendReceipt receipt = adapter.send(envelope("LIFT-1", "1",
DeviceCommandType.LIFT_TRANSFER,
"taskNo=30001|taskType=1|sourceFloor=3|targetFloor=7"));
assertEquals(SendReceipt.Status.ACCEPTED, receipt.getStatus());
assertEquals("DB602.0", runtime.addresses.get(0));
assertArrayEquals(new byte[] {0, 0, 117, 49, 0, 1, 0, 3,
0, 7, 0, 1, 0, (byte) 178}, runtime.payloads.get(0));
}
@Test
void conveyorAdapterMapsGoodsModeAndReadyFactsAndLegacyCommand() {
DeviceSsj device = conveyorDevice();
device.setTaskNo(30002);
device.setDw(1);
device.setKx(1);
device.setModel(1);
device.setTrayCode("TRAY-001");
device.setErrorCode(0);
FakeSsjRead runtime = new FakeSsjRead("GS1002", device);
Ft546ConveyorAdapter adapter = new Ft546ConveyorAdapter(runtime,
profile("GS1002", "CONVEYOR", "CONVEYOR_TRANSFER",
"DB41.0"),
(deviceId, commandId) -> false, contextProvider());
DeviceSnapshot snapshot = adapter.readSnapshot("GS1002");
SendReceipt receipt = adapter.send(envelope("CONV-1", "GS1002",
DeviceCommandType.CONVEYOR_TRANSFER,
"taskNo=30001|destination=2|completed=1"));
assertEquals("PRESENT", snapshot.getFacts().get("goodsState"));
assertEquals("1", snapshot.getFacts().get("mode"));
assertEquals("READY", snapshot.getFacts().get("readyState"));
assertEquals("TRAY-001", snapshot.getFacts().get("trayCode"));
assertEquals("0", snapshot.getFacts().get("faultCode"));
assertEquals(SendReceipt.Status.ACCEPTED, receipt.getStatus());
assertEquals("DB41.0", runtime.addresses.get(0));
assertArrayEquals(new byte[] {0, 0, 117, 49, 0, 2, 0, 1,
0, 0, 0, (byte) 169}, runtime.payloads.get(0));
}
@Test
void chargerAdapterMapsOccupancyChargingAndFaultFactsAndLegacyCommand() {
DeviceCdz device = chargerDevice();
device.setRwh(30003);
device.setStatus(1);
device.setModel(1);
device.setSpare1("1");
FakeCdzRead runtime = new FakeCdzRead("1", device);
Ft546ChargerAdapter adapter = new Ft546ChargerAdapter(runtime,
profile("1", "CHARGER", "CHARGE", "DB701.0"),
(deviceId, commandId) -> false, contextProvider());
DeviceSnapshot snapshot = adapter.readSnapshot("1");
SendReceipt receipt = adapter.send(envelope("CHARGE-1", "1",
DeviceCommandType.CHARGE, "enabled=1"));
assertEquals("OCCUPIED", snapshot.getFacts().get("occupancy"));
assertEquals("ON", snapshot.getFacts().get("charging"));
assertEquals("UNAVAILABLE", snapshot.getFacts().get("fault"));
assertEquals("070501", snapshot.getFacts().get("site"));
assertEquals(SendReceipt.Status.ACCEPTED, receipt.getStatus());
assertEquals("DB701.0", runtime.addresses.get(0));
assertArrayEquals(new byte[] {0, 1}, runtime.payloads.get(0));
}
@Test
void attemptedEquipmentWriteFailureIsUnknownAndBlocksRetry() {
FakeCdzRead runtime = new FakeCdzRead("1", chargerDevice());
runtime.writeResult = 0;
Ft546ChargerAdapter adapter = new Ft546ChargerAdapter(runtime,
profile("1", "CHARGER", "CHARGE", "DB701.0"),
(deviceId, commandId) -> false, contextProvider());
SendReceipt first = adapter.send(envelope("CHARGE-1", "1",
DeviceCommandType.CHARGE, "enabled=1"));
SendReceipt retry = adapter.send(envelope("CHARGE-2", "1",
DeviceCommandType.CHARGE, "enabled=1"));
assertEquals(SendReceipt.Status.ACCEPTED_UNKNOWN,
first.getStatus());
assertEquals(SendReceipt.Status.REJECTED, retry.getStatus());
assertEquals(1, runtime.payloads.size());
}
@Test
void factReadersPublishAConnectionDropAfterTheyWereConnected() {
FakeTsjRead lift = new FakeTsjRead("1", liftDevice());
FakeSsjRead conveyor = new FakeSsjRead("GS1002", conveyorDevice());
FakeCdzRead charger = new FakeCdzRead("1", chargerDevice());
DeviceSrgv shuttleDevice = new DeviceSrgv();
shuttleDevice.setDeviceId(1);
PlcSrgvRead shuttle = new PlcSrgvRead(shuttleDevice,
new PlcConnectInfo("127.0.0.1", 502));
shuttle.setIsConnect(true);
lift.setIsConnect(false);
conveyor.setIsConnect(false);
charger.setIsConnect(false);
shuttle.setIsConnect(false);
assertFalse(lift.IsConnect());
assertFalse(conveyor.IsConnect());
assertFalse(charger.IsConnect());
assertFalse(shuttle.IsConnect());
}
private static Ft546DeviceProfile profile(String deviceId,
String deviceType,
String capability,
String commandAddress) {
Ft546DeviceProfile profile = new Ft546DeviceProfile();
profile.setDeviceId(deviceId);
profile.setDeviceType(deviceType);
profile.setAdapterType("FT546_LEGACY_" + deviceType);
profile.setProfileVersion(1L);
profile.getCapabilities().add(capability);
profile.getRegisters().put("command", commandAddress);
return profile;
}
private static Ft546SnapshotContextProvider contextProvider() {
return deviceId -> new Ft546SnapshotContext("SNAP-" + deviceId,
"KEY-" + deviceId, 9L, 17L, NOW);
}
private static DeviceCommandEnvelope envelope(String commandId,
String deviceId,
DeviceCommandType type,
String payload) {
return new DeviceCommandEnvelope(commandId, "KEY-" + commandId,
deviceId, type, payload, 1, 9L, 17L);
}
private static DeviceTsj liftDevice() {
DeviceTsj device = new DeviceTsj();
device.setId(1L);
return device;
}
private static DeviceSsj conveyorDevice() {
DeviceSsj device = new DeviceSsj();
device.setDeviceCode("GS1002");
return device;
}
private static DeviceCdz chargerDevice() {
DeviceCdz device = new DeviceCdz();
device.setId(1);
device.setSiteId("070501");
return device;
}
private abstract static class RecordingRuntime {
private final List<String> addresses = new ArrayList<String>();
private final List<byte[]> payloads = new ArrayList<byte[]>();
private int writeResult = 1;
final int record(String address, byte[] payload) {
addresses.add(address);
payloads.add(payload.clone());
return writeResult;
}
}
private static final class FakeTsjRead extends PlcTsjRead {
private final RecordingRuntime recording = new RecordingRuntime() { };
private final List<String> addresses = recording.addresses;
private final List<byte[]> payloads = recording.payloads;
private FakeTsjRead(String id, DeviceTsj device) {
super(map(id, device), new PlcConnectInfo("127.0.0.1", 102));
setIsConnect(true);
}
@Override
public int write(String address, byte[] payload) {
return recording.record(address, payload);
}
}
private static final class FakeSsjRead extends PlcSsjRead {
private final RecordingRuntime recording = new RecordingRuntime() { };
private final List<String> addresses = recording.addresses;
private final List<byte[]> payloads = recording.payloads;
private FakeSsjRead(String id, DeviceSsj device) {
super(map(id, device), new PlcConnectInfo("127.0.0.1", 102));
setIsConnect(true);
}
@Override
public int write(String address, byte[] payload) {
return recording.record(address, payload);
}
}
private static final class FakeCdzRead extends PlcCdzRead {
private final List<String> addresses = new ArrayList<String>();
private final List<byte[]> payloads = new ArrayList<byte[]>();
private int writeResult = 1;
private FakeCdzRead(String id, DeviceCdz device) {
super(map(id, device), new PlcConnectInfo("127.0.0.1", 102));
setIsConnect(true);
}
@Override
public int write(String address, byte[] payload) {
addresses.add(address);
payloads.add(payload.clone());
return writeResult;
}
}
private static ConcurrentHashMap<String, DeviceTsj> map(
String id, DeviceTsj device) {
ConcurrentHashMap<String, DeviceTsj> map =
new ConcurrentHashMap<String, DeviceTsj>();
map.put(id, device);
return map;
}
private static ConcurrentHashMap<String, DeviceSsj> map(
String id, DeviceSsj device) {
ConcurrentHashMap<String, DeviceSsj> map =
new ConcurrentHashMap<String, DeviceSsj>();
map.put(id, device);
return map;
}
private static ConcurrentHashMap<String, DeviceCdz> map(
String id, DeviceCdz device) {
ConcurrentHashMap<String, DeviceCdz> map =
new ConcurrentHashMap<String, DeviceCdz>();
map.put(id, device);
return map;
}
}

View File

@@ -8,6 +8,7 @@ import com.ruoyi.project.ft546.config.Ft546CapabilityEvidence;
import com.ruoyi.project.ft546.config.Ft546CapabilityGate;
import com.ruoyi.project.ft546.config.Ft546DeviceProfile;
import com.ruoyi.project.ft546.device.Ft546DeviceRuntimeRegistry;
import com.ruoyi.project.ft546.device.Ft546MotionCommandGuard;
import com.ruoyi.schedule.plcRead.srgv.PlcSrgvRead;
import com.ruoyi.wcs.application.device.DeviceCommandEnvelope;
import com.ruoyi.wcs.application.device.SendReceipt;
@@ -241,7 +242,7 @@ class Ft546LegacySrgvAdapterTest {
private static Ft546LegacySrgvAdapter adapter(
FakePlcSrgvRead runtime, Ft546DeviceProfile profile,
Ft546CapabilityEvidence evidence,
Ft546LegacySrgvAdapter.MotionCommandGuard motionGuard) {
Ft546MotionCommandGuard motionGuard) {
Ft546DeviceRuntimeRegistry registry =
new Ft546DeviceRuntimeRegistry();
registry.register("1", runtime);