From ea0520a27a4528797edb8c8133655afcd4ae7896 Mon Sep 17 00:00:00 2001 From: zgw <13623675548@163.com> Date: Mon, 10 Aug 2026 15:58:58 +0800 Subject: [PATCH] feat(wcs): add device capabilities and snapshots --- .../domain/equipment/DeviceCapability.java | 9 ++ .../domain/equipment/DeviceDescriptor.java | 52 +++++++ .../wcs/domain/equipment/DeviceType.java | 8 + .../wcs/domain/equipment/VehicleSnapshot.java | 143 ++++++++++++++++++ .../ruoyi/wcs/port/out/DeviceRegistry.java | 9 ++ .../equipment/DeviceCapabilityTest.java | 70 +++++++++ 6 files changed, 291 insertions(+) create mode 100644 ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/equipment/DeviceCapability.java create mode 100644 ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/equipment/DeviceDescriptor.java create mode 100644 ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/equipment/DeviceType.java create mode 100644 ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/equipment/VehicleSnapshot.java create mode 100644 ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/DeviceRegistry.java create mode 100644 ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/equipment/DeviceCapabilityTest.java diff --git a/ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/equipment/DeviceCapability.java b/ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/equipment/DeviceCapability.java new file mode 100644 index 0000000..76f62ee --- /dev/null +++ b/ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/equipment/DeviceCapability.java @@ -0,0 +1,9 @@ +package com.ruoyi.wcs.domain.equipment; + +public enum DeviceCapability { + FOUR_WAY_MOVE, + LOAD_HANDLING, + LIFT_TRANSFER, + CONVEYOR_TRANSFER, + CHARGING +} diff --git a/ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/equipment/DeviceDescriptor.java b/ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/equipment/DeviceDescriptor.java new file mode 100644 index 0000000..695da28 --- /dev/null +++ b/ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/equipment/DeviceDescriptor.java @@ -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 capabilities; + + public DeviceDescriptor(String deviceId, DeviceType deviceType, + String floorCode, Set capabilities) { + this.deviceId = requireText(deviceId, "deviceId"); + this.deviceType = Objects.requireNonNull(deviceType, "deviceType"); + this.floorCode = requireText(floorCode, "floorCode"); + Objects.requireNonNull(capabilities, "capabilities"); + EnumSet 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 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(); + } +} diff --git a/ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/equipment/DeviceType.java b/ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/equipment/DeviceType.java new file mode 100644 index 0000000..ba37bc7 --- /dev/null +++ b/ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/equipment/DeviceType.java @@ -0,0 +1,8 @@ +package com.ruoyi.wcs.domain.equipment; + +public enum DeviceType { + FOUR_WAY_SHUTTLE, + LIFT, + CONVEYOR, + CHARGER +} diff --git a/ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/equipment/VehicleSnapshot.java b/ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/equipment/VehicleSnapshot.java new file mode 100644 index 0000000..37e1a53 --- /dev/null +++ b/ruoyi-wcs/src/main/java/com/ruoyi/wcs/domain/equipment/VehicleSnapshot.java @@ -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(); + } +} diff --git a/ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/DeviceRegistry.java b/ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/DeviceRegistry.java new file mode 100644 index 0000000..2832ada --- /dev/null +++ b/ruoyi-wcs/src/main/java/com/ruoyi/wcs/port/out/DeviceRegistry.java @@ -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 vehicles(); +} diff --git a/ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/equipment/DeviceCapabilityTest.java b/ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/equipment/DeviceCapabilityTest.java new file mode 100644 index 0000000..85b9036 --- /dev/null +++ b/ruoyi-wcs/src/test/java/com/ruoyi/wcs/domain/equipment/DeviceCapabilityTest.java @@ -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)); + } +}