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