test(ft546): verify standard WCS end to end
This commit is contained in:
@@ -2,15 +2,31 @@ package com.ruoyi.project.ft546.runtime;
|
||||
|
||||
import com.ruoyi.wcs.application.WcsRuntimeCoordinator;
|
||||
import com.ruoyi.wcs.application.device.CommandOutboxDispatcher;
|
||||
import com.ruoyi.wcs.application.device.CommandDeliveryRecorder;
|
||||
import com.ruoyi.wcs.application.device.DeviceExecutionService;
|
||||
import com.ruoyi.wcs.application.device.FeedbackReconciler;
|
||||
import com.ruoyi.wcs.application.device.SnapshotCollector;
|
||||
import com.ruoyi.wcs.application.device.WmsNotificationOutboxDispatcher;
|
||||
import com.ruoyi.wcs.application.device.WmsNotificationOutboxProjector;
|
||||
import com.ruoyi.wcs.application.dispatch.DispatchCoordinator;
|
||||
import com.ruoyi.wcs.application.recovery.RecoveryGate;
|
||||
import com.ruoyi.wcs.application.recovery.RecoveryApplicationService;
|
||||
import com.ruoyi.wcs.application.task.TransportOrderExecutionService;
|
||||
import com.ruoyi.wcs.domain.shared.IdGenerator;
|
||||
import com.ruoyi.wcs.domain.shared.WcsClock;
|
||||
import com.ruoyi.wcs.port.out.CommandOutboxRepository;
|
||||
import com.ruoyi.wcs.port.out.DeviceCommandGateway;
|
||||
import com.ruoyi.wcs.port.out.DeviceCommandRepository;
|
||||
import com.ruoyi.wcs.port.out.DeviceEventRepository;
|
||||
import com.ruoyi.wcs.port.out.DeviceExecutionRepository;
|
||||
import com.ruoyi.wcs.port.out.DispatchSnapshotProvider;
|
||||
import com.ruoyi.wcs.port.out.DomainEventPublisher;
|
||||
import com.ruoyi.wcs.port.out.RecoveryCaseRepository;
|
||||
import com.ruoyi.wcs.port.out.RecoveryCaseOpener;
|
||||
import com.ruoyi.wcs.port.out.ResourceReservationRepository;
|
||||
import com.ruoyi.wcs.port.out.TransportOrderExecutionRepository;
|
||||
import com.ruoyi.wcs.port.out.WmsNotificationGateway;
|
||||
import com.ruoyi.wcs.port.out.WmsNotificationOutboxRepository;
|
||||
import com.ruoyi.wcs.query.WcsOperationsQueryService;
|
||||
import com.ruoyi.wcs.query.WcsOperationsReadRepository;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
@@ -23,6 +39,71 @@ import org.springframework.context.annotation.Configuration;
|
||||
public class Ft546AdapterConfiguration {
|
||||
private static final int OUTBOX_BATCH_LIMIT = 100;
|
||||
|
||||
@Bean
|
||||
public WmsNotificationOutboxProjector wmsNotificationOutboxProjector(
|
||||
WmsNotificationOutboxRepository repository) {
|
||||
return new WmsNotificationOutboxProjector(repository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TransportOrderExecutionService transportOrderExecutionService(
|
||||
TransportOrderExecutionRepository repository,
|
||||
DomainEventPublisher eventPublisher,
|
||||
WmsNotificationOutboxProjector notificationProjector) {
|
||||
return new TransportOrderExecutionService(repository, eventPublisher,
|
||||
notificationProjector);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DeviceExecutionService deviceExecutionService(
|
||||
DeviceCommandRepository commandRepository,
|
||||
CommandOutboxRepository outboxRepository,
|
||||
DeviceExecutionRepository executionRepository,
|
||||
TransportOrderExecutionService lifecycle,
|
||||
IdGenerator idGenerator, WcsClock clock) {
|
||||
return new DeviceExecutionService(commandRepository, outboxRepository,
|
||||
executionRepository, lifecycle, idGenerator, clock::now);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public FeedbackReconciler feedbackReconciler(
|
||||
DeviceCommandRepository commandRepository,
|
||||
DeviceExecutionRepository executionRepository,
|
||||
DeviceEventRepository eventRepository,
|
||||
ResourceReservationRepository reservationRepository,
|
||||
RecoveryCaseOpener recoveryCaseOpener,
|
||||
TransportOrderExecutionService lifecycle) {
|
||||
return new FeedbackReconciler(commandRepository, executionRepository,
|
||||
eventRepository, reservationRepository, recoveryCaseOpener,
|
||||
lifecycle);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CommandDeliveryRecorder commandDeliveryRecorder(
|
||||
DeviceCommandRepository commandRepository,
|
||||
CommandOutboxRepository outboxRepository,
|
||||
RecoveryCaseOpener recoveryCaseOpener,
|
||||
ResourceReservationRepository reservationRepository) {
|
||||
return new CommandDeliveryRecorder(commandRepository, outboxRepository,
|
||||
recoveryCaseOpener, reservationRepository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CommandOutboxDispatcher commandOutboxDispatcher(
|
||||
CommandOutboxRepository outboxRepository,
|
||||
DeviceCommandGateway commandGateway,
|
||||
CommandDeliveryRecorder recorder) {
|
||||
return new CommandOutboxDispatcher(outboxRepository, commandGateway,
|
||||
recorder);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public WmsNotificationOutboxDispatcher wmsNotificationOutboxDispatcher(
|
||||
WmsNotificationOutboxRepository repository,
|
||||
WmsNotificationGateway gateway) {
|
||||
return new WmsNotificationOutboxDispatcher(repository, gateway);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public WcsRuntimeCoordinator ft546WcsRuntimeCoordinator(
|
||||
SnapshotCollector snapshotCollector,
|
||||
@@ -39,8 +120,11 @@ public class Ft546AdapterConfiguration {
|
||||
|
||||
@Bean
|
||||
public Ft546WcsScheduler ft546WcsScheduler(
|
||||
WcsRuntimeCoordinator coordinator) {
|
||||
return new Ft546WcsScheduler(coordinator);
|
||||
WcsRuntimeCoordinator coordinator,
|
||||
WmsNotificationOutboxDispatcher notificationDispatcher,
|
||||
WcsClock clock) {
|
||||
return new Ft546WcsScheduler(coordinator, notificationDispatcher,
|
||||
clock, OUTBOX_BATCH_LIMIT);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.ruoyi.project.ft546.runtime;
|
||||
|
||||
import com.ruoyi.wcs.application.RuntimeTickResult;
|
||||
import com.ruoyi.wcs.application.WcsRuntimeCoordinator;
|
||||
import com.ruoyi.wcs.application.device.WmsNotificationOutboxDispatcher;
|
||||
import com.ruoyi.wcs.domain.shared.WcsClock;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
|
||||
import java.util.Objects;
|
||||
@@ -11,16 +13,34 @@ import java.util.function.Supplier;
|
||||
|
||||
public final class Ft546WcsScheduler {
|
||||
private final Supplier<RuntimeTickResult> tick;
|
||||
private final Runnable notificationDispatch;
|
||||
private final AtomicBoolean tickInProgress = new AtomicBoolean();
|
||||
private final AtomicLong completedTickCount = new AtomicLong();
|
||||
private volatile RuntimeTickResult lastResult;
|
||||
|
||||
public Ft546WcsScheduler(WcsRuntimeCoordinator coordinator) {
|
||||
this(Objects.requireNonNull(coordinator, "coordinator")::tick);
|
||||
this(Objects.requireNonNull(coordinator, "coordinator")::tick,
|
||||
() -> { });
|
||||
}
|
||||
|
||||
public Ft546WcsScheduler(
|
||||
WcsRuntimeCoordinator coordinator,
|
||||
WmsNotificationOutboxDispatcher notificationDispatcher,
|
||||
WcsClock clock, int outboxBatchLimit) {
|
||||
this(Objects.requireNonNull(coordinator, "coordinator")::tick,
|
||||
notificationDispatch(notificationDispatcher, clock,
|
||||
outboxBatchLimit));
|
||||
}
|
||||
|
||||
Ft546WcsScheduler(Supplier<RuntimeTickResult> tick) {
|
||||
this(tick, () -> { });
|
||||
}
|
||||
|
||||
Ft546WcsScheduler(Supplier<RuntimeTickResult> tick,
|
||||
Runnable notificationDispatch) {
|
||||
this.tick = Objects.requireNonNull(tick, "tick");
|
||||
this.notificationDispatch = Objects.requireNonNull(
|
||||
notificationDispatch, "notificationDispatch");
|
||||
}
|
||||
|
||||
@Scheduled(fixedDelayString =
|
||||
@@ -29,13 +49,28 @@ public final class Ft546WcsScheduler {
|
||||
if (!tickInProgress.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
Throwable failure = null;
|
||||
try {
|
||||
lastResult = Objects.requireNonNull(tick.get(),
|
||||
"runtime coordinator returned null");
|
||||
completedTickCount.incrementAndGet();
|
||||
try {
|
||||
lastResult = Objects.requireNonNull(tick.get(),
|
||||
"runtime coordinator returned null");
|
||||
completedTickCount.incrementAndGet();
|
||||
} catch (RuntimeException | Error coreFailure) {
|
||||
failure = coreFailure;
|
||||
}
|
||||
try {
|
||||
notificationDispatch.run();
|
||||
} catch (RuntimeException | Error notificationFailure) {
|
||||
if (failure == null) {
|
||||
failure = notificationFailure;
|
||||
} else if (failure != notificationFailure) {
|
||||
failure.addSuppressed(notificationFailure);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
tickInProgress.set(false);
|
||||
}
|
||||
rethrow(failure);
|
||||
}
|
||||
|
||||
public long getCompletedTickCount() {
|
||||
@@ -49,4 +84,33 @@ public final class Ft546WcsScheduler {
|
||||
public boolean isTickInProgress() {
|
||||
return tickInProgress.get();
|
||||
}
|
||||
|
||||
private static void rethrow(Throwable failure) {
|
||||
if (failure == null) {
|
||||
return;
|
||||
}
|
||||
if (failure instanceof RuntimeException) {
|
||||
throw (RuntimeException) failure;
|
||||
}
|
||||
throw (Error) failure;
|
||||
}
|
||||
|
||||
private static int requirePositive(int value) {
|
||||
if (value <= 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"outboxBatchLimit must be positive");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static Runnable notificationDispatch(
|
||||
WmsNotificationOutboxDispatcher dispatcher, WcsClock clock,
|
||||
int batchLimit) {
|
||||
WmsNotificationOutboxDispatcher requiredDispatcher =
|
||||
Objects.requireNonNull(dispatcher, "notificationDispatcher");
|
||||
WcsClock requiredClock = Objects.requireNonNull(clock, "clock");
|
||||
int requiredLimit = requirePositive(batchLimit);
|
||||
return () -> requiredDispatcher.dispatchDue(requiredLimit,
|
||||
requiredClock.now());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,5 +11,7 @@ devices:
|
||||
taskType: "4002"
|
||||
path: "4004"
|
||||
trigger: "4021"
|
||||
crossFloorFlag: "4020"
|
||||
crossFloorTarget: "4022"
|
||||
status: "4068"
|
||||
execution: "4060"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -107,6 +107,40 @@ class Ft546StandardRuntimeTest {
|
||||
assertEquals(1L, scheduler.getCompletedTickCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void wmsOutboxStillDispatchesWhenCoreTickFails() {
|
||||
AtomicInteger notificationDispatches = new AtomicInteger();
|
||||
Ft546WcsScheduler scheduler = new Ft546WcsScheduler(() -> {
|
||||
throw new IllegalStateException("core tick failed");
|
||||
}, notificationDispatches::incrementAndGet);
|
||||
|
||||
IllegalStateException failure = assertThrows(
|
||||
IllegalStateException.class, scheduler::runScheduledTick);
|
||||
|
||||
assertEquals("core tick failed", failure.getMessage());
|
||||
assertEquals(1, notificationDispatches.get());
|
||||
assertEquals(0L, scheduler.getCompletedTickCount());
|
||||
assertFalse(scheduler.isTickInProgress());
|
||||
}
|
||||
|
||||
@Test
|
||||
void coreTickFailureRemainsPrimaryWhenNotificationDispatchAlsoFails() {
|
||||
Ft546WcsScheduler scheduler = new Ft546WcsScheduler(() -> {
|
||||
throw new IllegalStateException("core tick failed");
|
||||
}, () -> {
|
||||
throw new IllegalArgumentException("notification dispatch failed");
|
||||
});
|
||||
|
||||
IllegalStateException failure = assertThrows(
|
||||
IllegalStateException.class, scheduler::runScheduledTick);
|
||||
|
||||
assertEquals("core tick failed", failure.getMessage());
|
||||
assertEquals(1, failure.getSuppressed().length);
|
||||
assertEquals("notification dispatch failed",
|
||||
failure.getSuppressed()[0].getMessage());
|
||||
assertFalse(scheduler.isTickInProgress());
|
||||
}
|
||||
|
||||
@Test
|
||||
void legacyModeDoesNotRequireStandardRuntimePorts() {
|
||||
new ApplicationContextRunner()
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.ruoyi.wcs.domain.shared.WcsClock;
|
||||
import com.ruoyi.wcs.infrastructure.persistence.task.MyBatisTransportOrderRepository;
|
||||
import com.ruoyi.wcs.port.in.TransportOrderUseCase;
|
||||
import com.ruoyi.wcs.port.out.TransportOrderRepository;
|
||||
import com.ruoyi.wcs.port.out.TransportOrderExecutionRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
|
||||
@@ -28,6 +29,8 @@ class WcsCoreWiringTest {
|
||||
instanceof TransportOrderApplicationService);
|
||||
assertTrue(context.getBean(TransportOrderRepository.class)
|
||||
instanceof MyBatisTransportOrderRepository);
|
||||
assertTrue(context.getBean(TransportOrderExecutionRepository.class)
|
||||
instanceof MyBatisTransportOrderRepository);
|
||||
assertTrue(context.containsBeanDefinition("transportOrderMapper"));
|
||||
|
||||
WcsClock clock = context.getBean(WcsClock.class);
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"endpoint": "/wcs2Wms/api/receiveInMydoJob",
|
||||
"request": {
|
||||
"taskNo": "30001",
|
||||
"cargoSpaceId": "070502",
|
||||
"targetCargoSpaceId": null,
|
||||
"exitId": null,
|
||||
"trayCode": "TRAY-30001",
|
||||
"entranceId": "070501",
|
||||
"inOrOut": "10",
|
||||
"jobId": "JOB-30001",
|
||||
"roadwayId": "R1",
|
||||
"classId": "7",
|
||||
"priority": 5
|
||||
},
|
||||
"warehouseCode": "FT546",
|
||||
"topologyVersion": 7,
|
||||
"snapshotVersion": 11,
|
||||
"vehicle": {
|
||||
"deviceId": "1",
|
||||
"floorCode": "F7",
|
||||
"locationNodeId": "070501",
|
||||
"batteryPercent": 80
|
||||
},
|
||||
"nodes": [
|
||||
{"nodeId": "070501", "floorCode": "F7", "x": 0, "y": 0},
|
||||
{"nodeId": "070502", "floorCode": "F7", "x": 1, "y": 0}
|
||||
],
|
||||
"edges": [
|
||||
{"from": "070501", "to": "070502", "distanceCost": 1, "capacity": 1}
|
||||
],
|
||||
"commandParameters": {
|
||||
"taskNo": "30001",
|
||||
"taskType": "1"
|
||||
},
|
||||
"expectedCommandPayloadSuffix": "|vehicleId=1|route=070501>070502|taskNo=30001|taskType=1",
|
||||
"expectedProtectedResources": [
|
||||
"NODE:070501",
|
||||
"EDGE:070501:070502",
|
||||
"NODE:070502"
|
||||
],
|
||||
"expectedPlcWrites": [
|
||||
{"address": "4004", "kind": "INT32", "values": [70501, 70502, 0, 0, 0, 0, 0, 0]},
|
||||
{"address": "4000", "kind": "INT32", "values": [30001]},
|
||||
{"address": "4002", "kind": "INT16", "values": [1]},
|
||||
{"address": "4021", "kind": "INT16", "values": [1]}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"endpoint": "/wcs2Wms/api/receiveInMydoJob",
|
||||
"request": {
|
||||
"taskNo": "30003",
|
||||
"cargoSpaceId": "070503",
|
||||
"targetCargoSpaceId": null,
|
||||
"exitId": null,
|
||||
"trayCode": "TRAY-30003",
|
||||
"entranceId": "070501",
|
||||
"inOrOut": "10",
|
||||
"jobId": "JOB-30003",
|
||||
"roadwayId": "R1",
|
||||
"classId": "7",
|
||||
"priority": 5
|
||||
},
|
||||
"warehouseCode": "FT546",
|
||||
"topologyVersion": 9,
|
||||
"snapshotVersion": 13,
|
||||
"vehicle": {
|
||||
"deviceId": "1",
|
||||
"floorCode": "F7",
|
||||
"locationNodeId": "070501",
|
||||
"batteryPercent": 80
|
||||
},
|
||||
"nodes": [
|
||||
{"nodeId": "070501", "floorCode": "F7", "x": 0, "y": 0},
|
||||
{"nodeId": "070503", "floorCode": "F7", "x": 1, "y": 0}
|
||||
],
|
||||
"edges": [
|
||||
{"from": "070501", "to": "070503", "distanceCost": 1, "capacity": 1}
|
||||
],
|
||||
"commandParameters": {
|
||||
"taskNo": "30003",
|
||||
"taskType": "1",
|
||||
"stage": "1"
|
||||
},
|
||||
"expectedCommandPayloadSuffix": "|vehicleId=1|route=070501>070503|stage=1|taskNo=30003|taskType=1",
|
||||
"expectedProtectedResources": [
|
||||
"NODE:070501",
|
||||
"EDGE:070501:070503",
|
||||
"NODE:070503"
|
||||
],
|
||||
"expectedPlcWrites": [
|
||||
{"address": "4404", "kind": "INT32", "values": [70501, 70503, 0, 0, 0, 0, 0, 0]},
|
||||
{"address": "4422", "kind": "INT16", "values": [1]},
|
||||
{"address": "4402", "kind": "INT16", "values": [1]},
|
||||
{"address": "4400", "kind": "INT32", "values": [30003]}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"endpoint": "/wcs2Wms/api/receiveInMydoJob",
|
||||
"request": {
|
||||
"taskNo": "30004",
|
||||
"cargoSpaceId": "070504",
|
||||
"targetCargoSpaceId": null,
|
||||
"exitId": null,
|
||||
"trayCode": "TRAY-30004",
|
||||
"entranceId": "070503",
|
||||
"inOrOut": "10",
|
||||
"jobId": "JOB-30004",
|
||||
"roadwayId": "R1",
|
||||
"classId": "7",
|
||||
"priority": 5
|
||||
},
|
||||
"warehouseCode": "FT546",
|
||||
"topologyVersion": 10,
|
||||
"snapshotVersion": 14,
|
||||
"vehicle": {
|
||||
"deviceId": "1",
|
||||
"floorCode": "F7",
|
||||
"locationNodeId": "070503",
|
||||
"batteryPercent": 80
|
||||
},
|
||||
"nodes": [
|
||||
{"nodeId": "070503", "floorCode": "F7", "x": 0, "y": 0},
|
||||
{"nodeId": "070504", "floorCode": "F7", "x": 1, "y": 0}
|
||||
],
|
||||
"edges": [
|
||||
{"from": "070503", "to": "070504", "distanceCost": 1, "capacity": 1}
|
||||
],
|
||||
"commandParameters": {
|
||||
"taskNo": "30004",
|
||||
"taskType": "9",
|
||||
"stage": "2"
|
||||
},
|
||||
"expectedCommandPayloadSuffix": "|vehicleId=1|route=070503>070504|stage=2|taskNo=30004|taskType=9",
|
||||
"expectedProtectedResources": [
|
||||
"NODE:070503",
|
||||
"EDGE:070503:070504",
|
||||
"NODE:070504"
|
||||
],
|
||||
"expectedPlcWrites": [
|
||||
{"address": "4404", "kind": "INT32", "values": [70503, 70504, 0, 0, 0, 0, 0, 0]},
|
||||
{"address": "4422", "kind": "INT16", "values": [2]},
|
||||
{"address": "4423", "kind": "INT16", "values": [99]},
|
||||
{"address": "4402", "kind": "INT16", "values": [9]},
|
||||
{"address": "4400", "kind": "INT32", "values": [30004]}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"endpoint": "/wcs2Wms/api/receiveOutMydoJob",
|
||||
"request": {
|
||||
"taskNo": "30002",
|
||||
"cargoSpaceId": "070501",
|
||||
"targetCargoSpaceId": null,
|
||||
"exitId": "080501",
|
||||
"trayCode": "TRAY-30002",
|
||||
"entranceId": null,
|
||||
"inOrOut": "20",
|
||||
"jobId": "JOB-30002",
|
||||
"roadwayId": "R1",
|
||||
"classId": "7",
|
||||
"priority": 6
|
||||
},
|
||||
"warehouseCode": "FT546",
|
||||
"topologyVersion": 8,
|
||||
"snapshotVersion": 12,
|
||||
"vehicle": {
|
||||
"deviceId": "1",
|
||||
"floorCode": "F7",
|
||||
"locationNodeId": "070501",
|
||||
"batteryPercent": 82
|
||||
},
|
||||
"nodes": [
|
||||
{"nodeId": "070501", "floorCode": "F7", "x": 0, "y": 0},
|
||||
{"nodeId": "070507", "floorCode": "F7", "x": 1, "y": 0},
|
||||
{"nodeId": "080507", "floorCode": "F8", "x": 1, "y": 0},
|
||||
{"nodeId": "080501", "floorCode": "F8", "x": 2, "y": 0}
|
||||
],
|
||||
"edges": [
|
||||
{"from": "070501", "to": "070507", "distanceCost": 1, "capacity": 1},
|
||||
{"from": "070507", "to": "080507", "distanceCost": 1, "capacity": 1},
|
||||
{"from": "080507", "to": "080501", "distanceCost": 1, "capacity": 1}
|
||||
],
|
||||
"commandParameters": {
|
||||
"taskNo": "30002",
|
||||
"taskType": "8",
|
||||
"crossFloorTarget": "8"
|
||||
},
|
||||
"expectedCommandPayloadSuffix": "|vehicleId=1|route=070501>070507>080507>080501|crossFloorTarget=8|taskNo=30002|taskType=8",
|
||||
"expectedProtectedResources": [
|
||||
"NODE:070501",
|
||||
"EDGE:070501:070507",
|
||||
"NODE:070507",
|
||||
"EDGE:070507:080507",
|
||||
"NODE:080507",
|
||||
"EDGE:080501:080507",
|
||||
"NODE:080501"
|
||||
],
|
||||
"expectedPlcWrites": [
|
||||
{"address": "4004", "kind": "INT32", "values": [70501, 70507, 80507, 80501, 0, 0, 0, 0]},
|
||||
{"address": "4000", "kind": "INT32", "values": [30002]},
|
||||
{"address": "4002", "kind": "INT16", "values": [8]},
|
||||
{"address": "4021", "kind": "INT16", "values": [1]},
|
||||
{"address": "4020", "kind": "INT16", "values": [1]},
|
||||
{"address": "4022", "kind": "INT16", "values": [8]}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user