feat(wcs): add device capabilities and snapshots
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
package com.ruoyi.wcs.domain.equipment;
|
||||||
|
|
||||||
|
public enum DeviceCapability {
|
||||||
|
FOUR_WAY_MOVE,
|
||||||
|
LOAD_HANDLING,
|
||||||
|
LIFT_TRANSFER,
|
||||||
|
CONVEYOR_TRANSFER,
|
||||||
|
CHARGING
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.ruoyi.wcs.domain.equipment;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public final class DeviceDescriptor {
|
||||||
|
private final String deviceId;
|
||||||
|
private final DeviceType deviceType;
|
||||||
|
private final String floorCode;
|
||||||
|
private final Set<DeviceCapability> capabilities;
|
||||||
|
|
||||||
|
public DeviceDescriptor(String deviceId, DeviceType deviceType,
|
||||||
|
String floorCode, Set<DeviceCapability> capabilities) {
|
||||||
|
this.deviceId = requireText(deviceId, "deviceId");
|
||||||
|
this.deviceType = Objects.requireNonNull(deviceType, "deviceType");
|
||||||
|
this.floorCode = requireText(floorCode, "floorCode");
|
||||||
|
Objects.requireNonNull(capabilities, "capabilities");
|
||||||
|
EnumSet<DeviceCapability> copied = capabilities.isEmpty()
|
||||||
|
? EnumSet.noneOf(DeviceCapability.class)
|
||||||
|
: EnumSet.copyOf(capabilities);
|
||||||
|
this.capabilities = Collections.unmodifiableSet(copied);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDeviceId() {
|
||||||
|
return deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DeviceType getDeviceType() {
|
||||||
|
return deviceType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFloorCode() {
|
||||||
|
return floorCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<DeviceCapability> getCapabilities() {
|
||||||
|
return capabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean supports(DeviceCapability capability) {
|
||||||
|
return capabilities.contains(Objects.requireNonNull(capability, "capability"));
|
||||||
|
}
|
||||||
|
|
||||||
|
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,8 @@
|
|||||||
|
package com.ruoyi.wcs.domain.equipment;
|
||||||
|
|
||||||
|
public enum DeviceType {
|
||||||
|
FOUR_WAY_SHUTTLE,
|
||||||
|
LIFT,
|
||||||
|
CONVEYOR,
|
||||||
|
CHARGER
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
package com.ruoyi.wcs.domain.equipment;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public final class VehicleSnapshot {
|
||||||
|
public enum ConnectionState {
|
||||||
|
ONLINE,
|
||||||
|
OFFLINE,
|
||||||
|
UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum OperatingMode {
|
||||||
|
AUTO,
|
||||||
|
MANUAL
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum HealthState {
|
||||||
|
NORMAL,
|
||||||
|
FAULT,
|
||||||
|
UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum OccupationState {
|
||||||
|
IDLE,
|
||||||
|
BUSY,
|
||||||
|
CHARGING,
|
||||||
|
UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
private final DeviceDescriptor descriptor;
|
||||||
|
private final ConnectionState connectionState;
|
||||||
|
private final OperatingMode operatingMode;
|
||||||
|
private final HealthState healthState;
|
||||||
|
private final OccupationState occupationState;
|
||||||
|
private final String locationNodeId;
|
||||||
|
private final String loadId;
|
||||||
|
private final int batteryPercent;
|
||||||
|
private final String currentCommandId;
|
||||||
|
private final String floorCode;
|
||||||
|
private final Instant snapshotTime;
|
||||||
|
|
||||||
|
public VehicleSnapshot(DeviceDescriptor descriptor,
|
||||||
|
ConnectionState connectionState,
|
||||||
|
OperatingMode operatingMode,
|
||||||
|
HealthState healthState,
|
||||||
|
OccupationState occupationState,
|
||||||
|
String locationNodeId,
|
||||||
|
String loadId,
|
||||||
|
int batteryPercent,
|
||||||
|
String currentCommandId,
|
||||||
|
String floorCode,
|
||||||
|
Instant snapshotTime) {
|
||||||
|
this.descriptor = Objects.requireNonNull(descriptor, "descriptor");
|
||||||
|
this.connectionState = Objects.requireNonNull(connectionState, "connectionState");
|
||||||
|
this.operatingMode = Objects.requireNonNull(operatingMode, "operatingMode");
|
||||||
|
this.healthState = Objects.requireNonNull(healthState, "healthState");
|
||||||
|
this.occupationState = Objects.requireNonNull(occupationState, "occupationState");
|
||||||
|
this.locationNodeId = requireText(locationNodeId, "locationNodeId");
|
||||||
|
this.loadId = normalizeOptional(loadId);
|
||||||
|
if (batteryPercent < 0 || batteryPercent > 100) {
|
||||||
|
throw new IllegalArgumentException("batteryPercent must be between 0 and 100");
|
||||||
|
}
|
||||||
|
this.batteryPercent = batteryPercent;
|
||||||
|
this.currentCommandId = normalizeOptional(currentCommandId);
|
||||||
|
this.floorCode = requireText(floorCode, "floorCode");
|
||||||
|
this.snapshotTime = Objects.requireNonNull(snapshotTime, "snapshotTime");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDispatchable(int minimumBattery) {
|
||||||
|
if (minimumBattery < 0 || minimumBattery > 100) {
|
||||||
|
throw new IllegalArgumentException("minimumBattery must be between 0 and 100");
|
||||||
|
}
|
||||||
|
return connectionState == ConnectionState.ONLINE
|
||||||
|
&& operatingMode == OperatingMode.AUTO
|
||||||
|
&& healthState == HealthState.NORMAL
|
||||||
|
&& occupationState == OccupationState.IDLE
|
||||||
|
&& batteryPercent >= minimumBattery;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean supports(DeviceCapability capability) {
|
||||||
|
return descriptor.supports(capability);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getVehicleId() {
|
||||||
|
return descriptor.getDeviceId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public DeviceDescriptor getDescriptor() {
|
||||||
|
return descriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ConnectionState getConnectionState() {
|
||||||
|
return connectionState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OperatingMode getOperatingMode() {
|
||||||
|
return operatingMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HealthState getHealthState() {
|
||||||
|
return healthState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OccupationState getOccupationState() {
|
||||||
|
return occupationState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLocationNodeId() {
|
||||||
|
return locationNodeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLoadId() {
|
||||||
|
return loadId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getBatteryPercent() {
|
||||||
|
return batteryPercent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCurrentCommandId() {
|
||||||
|
return currentCommandId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFloorCode() {
|
||||||
|
return floorCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Instant getSnapshotTime() {
|
||||||
|
return snapshotTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizeOptional(String value) {
|
||||||
|
return value == null || value.trim().isEmpty() ? null : value.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.ruoyi.wcs.port.out;
|
||||||
|
|
||||||
|
import com.ruoyi.wcs.domain.equipment.DeviceDescriptor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface DeviceRegistry {
|
||||||
|
List<DeviceDescriptor> vehicles();
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package com.ruoyi.wcs.domain.equipment;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.CsvSource;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
|
||||||
|
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 DeviceCapabilityTest {
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@CsvSource({
|
||||||
|
"ONLINE,AUTO,NORMAL,IDLE,80,true",
|
||||||
|
"OFFLINE,AUTO,NORMAL,IDLE,80,false",
|
||||||
|
"ONLINE,MANUAL,NORMAL,IDLE,80,false",
|
||||||
|
"ONLINE,AUTO,FAULT,IDLE,80,false",
|
||||||
|
"ONLINE,AUTO,NORMAL,BUSY,80,false",
|
||||||
|
"ONLINE,AUTO,NORMAL,IDLE,20,false",
|
||||||
|
"ONLINE,AUTO,NORMAL,IDLE,30,true"
|
||||||
|
})
|
||||||
|
void dispatchabilityUsesOrthogonalStateDimensions(
|
||||||
|
String connection, String mode, String health,
|
||||||
|
String occupation, int battery, boolean expected) {
|
||||||
|
VehicleSnapshot snapshot = snapshot(connection, mode, health,
|
||||||
|
occupation, battery);
|
||||||
|
|
||||||
|
assertEquals(expected, snapshot.isDispatchable(30));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void descriptorMatchesOnlyDeclaredCapabilities() {
|
||||||
|
DeviceDescriptor descriptor = descriptor();
|
||||||
|
|
||||||
|
assertTrue(descriptor.supports(DeviceCapability.FOUR_WAY_MOVE));
|
||||||
|
assertTrue(descriptor.supports(DeviceCapability.LOAD_HANDLING));
|
||||||
|
assertFalse(descriptor.supports(DeviceCapability.LIFT_TRANSFER));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void descriptorCapabilitySetCannotBeMutated() {
|
||||||
|
DeviceDescriptor descriptor = descriptor();
|
||||||
|
|
||||||
|
assertThrows(UnsupportedOperationException.class,
|
||||||
|
() -> descriptor.getCapabilities().add(DeviceCapability.LIFT_TRANSFER));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static VehicleSnapshot snapshot(String connection, String mode,
|
||||||
|
String health, String occupation,
|
||||||
|
int battery) {
|
||||||
|
return new VehicleSnapshot(descriptor(),
|
||||||
|
VehicleSnapshot.ConnectionState.valueOf(connection),
|
||||||
|
VehicleSnapshot.OperatingMode.valueOf(mode),
|
||||||
|
VehicleSnapshot.HealthState.valueOf(health),
|
||||||
|
VehicleSnapshot.OccupationState.valueOf(occupation),
|
||||||
|
"A", null, battery, null, "F1",
|
||||||
|
Instant.parse("2026-08-04T00:00:00Z"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DeviceDescriptor descriptor() {
|
||||||
|
return new DeviceDescriptor("V1", DeviceType.FOUR_WAY_SHUTTLE, "F1",
|
||||||
|
EnumSet.of(DeviceCapability.FOUR_WAY_MOVE,
|
||||||
|
DeviceCapability.LOAD_HANDLING));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user