refactor(ft546): share pure PLC command encoding

This commit is contained in:
zgw
2026-08-12 13:27:30 +08:00
parent d30fa0bd1c
commit 778444e5ea
28 changed files with 1021 additions and 265 deletions

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi</artifactId>
<version>3.8.9</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-ft546-protocol</artifactId>
<packaging>jar</packaging>
<description>Pure FT546 command protocol model and encoder</description>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,78 @@
package com.ruoyi.ft546.protocol;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
public class Ft546DeviceProfile {
private String deviceId;
private String deviceType;
private String adapterType;
private String ip;
private Integer port;
private Integer station;
private Set<String> capabilities = new LinkedHashSet<String>();
private Map<String, String> registers =
new LinkedHashMap<String, String>();
private long profileVersion;
private String protocolCorpusHash;
public String getDeviceId() { return deviceId; }
public void setDeviceId(String deviceId) { this.deviceId = deviceId; }
public String getDeviceType() { return deviceType; }
public void setDeviceType(String deviceType) { this.deviceType = deviceType; }
public String getAdapterType() { return adapterType; }
public void setAdapterType(String adapterType) { this.adapterType = adapterType; }
public String getIp() { return ip; }
public void setIp(String ip) { this.ip = ip; }
public Integer getPort() { return port; }
public void setPort(Integer port) { this.port = port; }
public Integer getStation() { return station; }
public void setStation(Integer station) { this.station = station; }
public Set<String> getCapabilities() {
return Collections.unmodifiableSet(capabilities);
}
public void setCapabilities(Set<String> capabilities) {
this.capabilities = capabilities == null
? new LinkedHashSet<String>()
: new LinkedHashSet<String>(capabilities);
}
public void addCapability(String capability) {
if (capability == null || capability.trim().isEmpty()) {
throw new IllegalArgumentException(
"capability must not be blank");
}
capabilities.add(capability.trim());
}
public void removeCapability(String capability) {
capabilities.remove(capability);
}
public Map<String, String> getRegisters() {
return Collections.unmodifiableMap(registers);
}
public void setRegisters(Map<String, String> registers) {
this.registers = registers == null
? new LinkedHashMap<String, String>()
: new LinkedHashMap<String, String>(registers);
}
public void putRegister(String name, String address) {
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException(
"register name must not be blank");
}
registers.put(name.trim(), address);
}
public void removeRegister(String name) {
registers.remove(name);
}
public long getProfileVersion() { return profileVersion; }
public void setProfileVersion(long profileVersion) {
this.profileVersion = profileVersion;
}
public String getProtocolCorpusHash() { return protocolCorpusHash; }
public void setProtocolCorpusHash(String protocolCorpusHash) {
this.protocolCorpusHash = protocolCorpusHash;
}
}

View File

@@ -0,0 +1,46 @@
package com.ruoyi.ft546.protocol;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class Ft546EncodedCommand {
private final String deviceId;
private final String parentTaskId;
private final String phase;
private final String commandKey;
private final long routeVersion;
private final int taskNo;
private final short taskType;
private final List<Integer> pathPoints;
private final List<Ft546RegisterWrite> writes;
private final int triggerValue;
Ft546EncodedCommand(Ft546SrgvCommandIntent intent,
List<Ft546RegisterWrite> writes,
int triggerValue) {
this.deviceId = intent.getDeviceId();
this.parentTaskId = intent.getParentTaskId();
this.phase = intent.getPhase();
this.commandKey = intent.getCommandKey();
this.routeVersion = intent.getRouteVersion();
this.taskNo = intent.getTaskNo();
this.taskType = intent.getTaskType();
this.pathPoints = Collections.unmodifiableList(
new ArrayList<Integer>(intent.getPathPoints()));
this.writes = Collections.unmodifiableList(
new ArrayList<Ft546RegisterWrite>(writes));
this.triggerValue = triggerValue;
}
public String getDeviceId() { return deviceId; }
public String getParentTaskId() { return parentTaskId; }
public String getPhase() { return phase; }
public String getCommandKey() { return commandKey; }
public long getRouteVersion() { return routeVersion; }
public int getTaskNo() { return taskNo; }
public short getTaskType() { return taskType; }
public List<Integer> getPathPoints() { return pathPoints; }
public List<Ft546RegisterWrite> getWrites() { return writes; }
public int getTriggerValue() { return triggerValue; }
}

View File

@@ -0,0 +1,45 @@
package com.ruoyi.ft546.protocol;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class Ft546RegisterWrite {
public enum DataType { INT16, INT32 }
private final String address;
private final DataType dataType;
private final List<Integer> values;
private final int writeSequence;
private final boolean trigger;
Ft546RegisterWrite(String address, DataType dataType,
List<Integer> values, int writeSequence,
boolean trigger) {
if (address == null || address.trim().isEmpty()) {
throw new IllegalArgumentException("address must not be blank");
}
if (dataType == null) {
throw new IllegalArgumentException("dataType must not be null");
}
if (values == null || values.isEmpty()) {
throw new IllegalArgumentException("values must not be empty");
}
if (writeSequence <= 0) {
throw new IllegalArgumentException(
"writeSequence must be positive");
}
this.address = address.trim();
this.dataType = dataType;
this.values = Collections.unmodifiableList(
new ArrayList<Integer>(values));
this.writeSequence = writeSequence;
this.trigger = trigger;
}
public String getAddress() { return address; }
public DataType getDataType() { return dataType; }
public List<Integer> getValues() { return values; }
public int getWriteSequence() { return writeSequence; }
public boolean isTrigger() { return trigger; }
}

View File

@@ -0,0 +1,153 @@
package com.ruoyi.ft546.protocol;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
public final class Ft546SrgvCommandEncoder {
public Ft546EncodedCommand encode(Ft546SrgvCommandIntent intent,
Ft546DeviceProfile profile,
Set<String> capabilities) {
if (intent == null) {
throw new IllegalArgumentException("intent must not be null");
}
if (profile == null) {
throw new IllegalArgumentException("profile must not be null");
}
if (capabilities == null) {
throw new IllegalArgumentException(
"capabilities must not be null");
}
int[] route = route(intent.getPathPoints());
Integer stage = intent.getTwoStageNumber();
if (stage == null) {
if (!capabilities.contains("POINT_TO_POINT_COMMAND")) {
throw new IllegalStateException(
"POINT_TO_POINT_COMMAND is not enabled");
}
return main(intent, profile, route);
}
if (!capabilities.contains("TWO_STAGE_ATOMIC_TASK")) {
throw new IllegalStateException(
"TWO_STAGE_ATOMIC_TASK is not enabled");
}
if (stage.intValue() == 1) {
return secondStageOne(intent, profile, route);
}
if (stage.intValue() == 2) {
return secondStageTwo(intent, profile, route);
}
throw new IllegalArgumentException("stage must be 1 or 2");
}
private static Ft546EncodedCommand main(Ft546SrgvCommandIntent intent,
Ft546DeviceProfile profile,
int[] route) {
List<Ft546RegisterWrite> writes =
new ArrayList<Ft546RegisterWrite>();
add(writes, profile, "path", Ft546RegisterWrite.DataType.INT32,
false, route);
add(writes, profile, "taskNo", Ft546RegisterWrite.DataType.INT32,
false, intent.getTaskNo());
add(writes, profile, "taskType", Ft546RegisterWrite.DataType.INT16,
false, intent.getTaskType());
add(writes, profile, "trigger", Ft546RegisterWrite.DataType.INT16,
true, 1);
if (intent.getTaskType() == 8) {
if (intent.getCrossFloorTarget() == null) {
throw new IllegalArgumentException(
"crossFloorTarget is required");
}
add(writes, profile, "crossFloorFlag",
Ft546RegisterWrite.DataType.INT16, false, 1);
add(writes, profile, "crossFloorTarget",
Ft546RegisterWrite.DataType.INT16, false,
intent.getCrossFloorTarget());
}
return new Ft546EncodedCommand(intent, writes, 1);
}
private static Ft546EncodedCommand secondStageOne(
Ft546SrgvCommandIntent intent, Ft546DeviceProfile profile,
int[] route) {
List<Ft546RegisterWrite> writes =
new ArrayList<Ft546RegisterWrite>();
add(writes, profile, "secondPath",
Ft546RegisterWrite.DataType.INT32, false, route);
add(writes, profile, "secondStageFlag",
Ft546RegisterWrite.DataType.INT16, true, 1);
add(writes, profile, "secondTaskType",
Ft546RegisterWrite.DataType.INT16, false,
intent.getTaskType());
add(writes, profile, "secondTaskNo",
Ft546RegisterWrite.DataType.INT32, false,
intent.getTaskNo());
return new Ft546EncodedCommand(intent, writes, 1);
}
private static Ft546EncodedCommand secondStageTwo(
Ft546SrgvCommandIntent intent, Ft546DeviceProfile profile,
int[] route) {
List<Ft546RegisterWrite> writes =
new ArrayList<Ft546RegisterWrite>();
add(writes, profile, "secondPath",
Ft546RegisterWrite.DataType.INT32, false, route);
add(writes, profile, "secondStageFlag",
Ft546RegisterWrite.DataType.INT16, true, 2);
add(writes, profile, "secondComplete",
Ft546RegisterWrite.DataType.INT16, false, 99);
add(writes, profile, "secondTaskType",
Ft546RegisterWrite.DataType.INT16, false, 9);
add(writes, profile, "secondTaskNo",
Ft546RegisterWrite.DataType.INT32, false,
intent.getTaskNo());
return new Ft546EncodedCommand(intent, writes, 2);
}
private static void add(List<Ft546RegisterWrite> writes,
Ft546DeviceProfile profile, String register,
Ft546RegisterWrite.DataType dataType,
boolean trigger, int... values) {
List<Integer> boxed = new ArrayList<Integer>(values.length);
for (int value : values) {
if (dataType == Ft546RegisterWrite.DataType.INT16
&& (value < Short.MIN_VALUE || value > Short.MAX_VALUE)) {
throw new IllegalArgumentException(
register + " value is not an int16");
}
boxed.add(value);
}
writes.add(new Ft546RegisterWrite(register(profile, register),
dataType, boxed, writes.size() + 1, trigger));
}
private static int[] route(List<Integer> points) {
if (points.size() < 2 || points.size() > 8) {
throw new IllegalArgumentException(
"legacy route must contain between 2 and 8 command points");
}
int[] result = new int[8];
for (int index = 0; index < points.size(); index++) {
Integer point = points.get(index);
if (point == null || point.intValue() <= 0) {
throw new IllegalArgumentException(
"route node must be positive");
}
if (point.intValue() > 999999) {
throw new IllegalArgumentException(
"route node exceeds six digits");
}
result[index] = point.intValue();
}
return result;
}
private static String register(Ft546DeviceProfile profile, String name) {
String address = profile.getRegisters().get(name);
if (address == null || address.trim().isEmpty()) {
throw new IllegalArgumentException(
"profile register is missing: " + name);
}
return address.trim();
}
}

View File

@@ -0,0 +1,64 @@
package com.ruoyi.ft546.protocol;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
public final class Ft546SrgvCommandIntent {
private final String deviceId;
private final String parentTaskId;
private final String phase;
private final String commandKey;
private final long routeVersion;
private final int taskNo;
private final short taskType;
private final List<Integer> pathPoints;
private final Short crossFloorTarget;
private final Integer twoStageNumber;
public Ft546SrgvCommandIntent(String deviceId, String parentTaskId,
String phase, String commandKey,
long routeVersion, int taskNo,
short taskType, List<Integer> pathPoints,
Short crossFloorTarget,
Integer twoStageNumber) {
this.deviceId = requireText(deviceId, "deviceId");
this.parentTaskId = requireText(parentTaskId, "parentTaskId");
this.phase = requireText(phase, "phase");
this.commandKey = requireText(commandKey, "commandKey");
if (routeVersion <= 0L) {
throw new IllegalArgumentException(
"routeVersion must be positive");
}
if (taskNo <= 0) {
throw new IllegalArgumentException("taskNo must be positive");
}
this.routeVersion = routeVersion;
this.taskNo = taskNo;
this.taskType = taskType;
this.pathPoints = Collections.unmodifiableList(
new ArrayList<Integer>(Objects.requireNonNull(pathPoints,
"pathPoints")));
this.crossFloorTarget = crossFloorTarget;
this.twoStageNumber = twoStageNumber;
}
public String getDeviceId() { return deviceId; }
public String getParentTaskId() { return parentTaskId; }
public String getPhase() { return phase; }
public String getCommandKey() { return commandKey; }
public long getRouteVersion() { return routeVersion; }
public int getTaskNo() { return taskNo; }
public short getTaskType() { return taskType; }
public List<Integer> getPathPoints() { return pathPoints; }
public Short getCrossFloorTarget() { return crossFloorTarget; }
public Integer getTwoStageNumber() { return twoStageNumber; }
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,278 @@
package com.ruoyi.ft546.protocol;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static com.ruoyi.ft546.protocol.Ft546RegisterWrite.DataType.INT16;
import static com.ruoyi.ft546.protocol.Ft546RegisterWrite.DataType.INT32;
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 Ft546SrgvCommandEncoderTest {
private final Ft546SrgvCommandEncoder encoder =
new Ft546SrgvCommandEncoder();
@Test
void ordinaryIntentEncodesTheExactLegacySequence() {
Ft546EncodedCommand encoded = encoder.encode(
intent((short) 1, points(70501, 70502), null, null),
fullProfile(), capabilities("POINT_TO_POINT_COMMAND"));
assertWrites(encoded,
write(1, "4004", INT32, false,
70501, 70502, 0, 0, 0, 0, 0, 0),
write(2, "4000", INT32, false, 30001),
write(3, "4002", INT16, false, 1),
write(4, "4021", INT16, true, 1));
assertEquals("1", encoded.getDeviceId());
assertEquals("PARENT-1", encoded.getParentTaskId());
assertEquals("LOADED_TO_PLACE", encoded.getPhase());
assertEquals("KEY-1", encoded.getCommandKey());
assertEquals(3L, encoded.getRouteVersion());
assertEquals(30001, encoded.getTaskNo());
assertEquals((short) 1, encoded.getTaskType());
assertEquals(points(70501, 70502), encoded.getPathPoints());
assertEquals(1, encoded.getTriggerValue());
}
@Test
void crossFloorIntentEncodesTheExactLegacyPostTriggerSequence() {
Ft546EncodedCommand encoded = encoder.encode(
intent((short) 8, points(70501, 70502),
Short.valueOf((short) 8), null),
fullProfile(), capabilities("POINT_TO_POINT_COMMAND"));
assertWrites(encoded,
write(1, "4004", INT32, false,
70501, 70502, 0, 0, 0, 0, 0, 0),
write(2, "4000", INT32, false, 30001),
write(3, "4002", INT16, false, 8),
write(4, "4021", INT16, true, 1),
write(5, "4020", INT16, false, 1),
write(6, "4022", INT16, false, 8));
}
@Test
void twoStageOneEncodesTheExactLegacySequence() {
Ft546EncodedCommand encoded = encoder.encode(
intent((short) 1, points(70501, 70503), null, 1),
fullProfile(), capabilities("TWO_STAGE_ATOMIC_TASK"));
assertWrites(encoded,
write(1, "4404", INT32, false,
70501, 70503, 0, 0, 0, 0, 0, 0),
write(2, "4422", INT16, true, 1),
write(3, "4402", INT16, false, 1),
write(4, "4400", INT32, false, 30001));
assertEquals(1, encoded.getTriggerValue());
}
@Test
void twoStageTwoEncodesTheExactLegacySequence() {
Ft546EncodedCommand encoded = encoder.encode(
intent((short) 9, points(70503, 70504), null, 2),
fullProfile(), capabilities("TWO_STAGE_ATOMIC_TASK"));
assertWrites(encoded,
write(1, "4404", INT32, false,
70503, 70504, 0, 0, 0, 0, 0, 0),
write(2, "4422", INT16, true, 2),
write(3, "4423", INT16, false, 99),
write(4, "4402", INT16, false, 9),
write(5, "4400", INT32, false, 30001));
assertEquals(2, encoded.getTriggerValue());
}
@Test
void missingRegisterFailsClosedBeforeEncoding() {
Ft546DeviceProfile profile = fullProfile();
Map<String, String> registers = new LinkedHashMap<String, String>(
profile.getRegisters());
registers.remove("trigger");
profile.setRegisters(registers);
IllegalArgumentException failure = assertThrows(
IllegalArgumentException.class,
() -> encoder.encode(intent((short) 1,
points(70501, 70502), null, null),
profile, capabilities("POINT_TO_POINT_COMMAND")));
assertEquals("profile register is missing: trigger",
failure.getMessage());
}
@Test
void routeLengthTwoAndEightAreAcceptedAndPaddedExactly() {
Ft546EncodedCommand two = encodeRoute(points(1, 2));
Ft546EncodedCommand eight = encodeRoute(
points(1, 2, 3, 4, 5, 6, 7, 8));
assertEquals(points(1, 2, 0, 0, 0, 0, 0, 0),
two.getWrites().get(0).getValues());
assertEquals(points(1, 2, 3, 4, 5, 6, 7, 8),
eight.getWrites().get(0).getValues());
}
@Test
void routeLengthOneAndNineAreRejected() {
assertRouteRejected(points(1));
assertRouteRejected(points(1, 2, 3, 4, 5, 6, 7, 8, 9));
}
@Test
void routePointBeyondSixDigitsIsRejected() {
IllegalArgumentException failure = assertThrows(
IllegalArgumentException.class,
() -> encodeRoute(points(1, 1000000)));
assertEquals("route node exceeds six digits", failure.getMessage());
}
@Test
void capabilityGatesMainAndTwoStageIndependently() {
IllegalStateException main = assertThrows(IllegalStateException.class,
() -> encoder.encode(intent((short) 1,
points(1, 2), null, null),
fullProfile(), Collections.<String>emptySet()));
IllegalStateException stage = assertThrows(IllegalStateException.class,
() -> encoder.encode(intent((short) 1,
points(1, 2), null, 1),
fullProfile(), capabilities(
"POINT_TO_POINT_COMMAND")));
assertEquals("POINT_TO_POINT_COMMAND is not enabled",
main.getMessage());
assertEquals("TWO_STAGE_ATOMIC_TASK is not enabled",
stage.getMessage());
}
@Test
void profileIntentAndEncodedCollectionsAreImmutableSnapshots() {
Ft546DeviceProfile profile = fullProfile();
List<Integer> mutablePoints = new ArrayList<Integer>(
points(70501, 70502));
Ft546SrgvCommandIntent intent = intent((short) 1,
mutablePoints, null, null);
mutablePoints.set(0, 99999);
Ft546EncodedCommand encoded = encoder.encode(intent, profile,
capabilities("POINT_TO_POINT_COMMAND"));
assertThrows(UnsupportedOperationException.class,
() -> profile.getRegisters().put("path", "9999"));
assertThrows(UnsupportedOperationException.class,
() -> profile.getCapabilities().add("UNSAFE"));
assertThrows(UnsupportedOperationException.class,
() -> intent.getPathPoints().add(3));
assertThrows(UnsupportedOperationException.class,
() -> encoded.getWrites().clear());
assertThrows(UnsupportedOperationException.class,
() -> encoded.getWrites().get(0).getValues().set(0, 99999));
assertEquals(Integer.valueOf(70501),
encoded.getPathPoints().get(0));
}
private Ft546EncodedCommand encodeRoute(List<Integer> route) {
return encoder.encode(intent((short) 1, route, null, null),
fullProfile(), capabilities("POINT_TO_POINT_COMMAND"));
}
private void assertRouteRejected(List<Integer> route) {
IllegalArgumentException failure = assertThrows(
IllegalArgumentException.class, () -> encodeRoute(route));
assertEquals(
"legacy route must contain between 2 and 8 command points",
failure.getMessage());
}
private static Ft546SrgvCommandIntent intent(short taskType,
List<Integer> route,
Short crossFloorTarget,
Integer stage) {
return new Ft546SrgvCommandIntent("1", "PARENT-1",
"LOADED_TO_PLACE", "KEY-1", 3L, 30001, taskType,
route, crossFloorTarget, stage);
}
private static Ft546DeviceProfile fullProfile() {
Ft546DeviceProfile profile = new Ft546DeviceProfile();
profile.setDeviceId("1");
profile.setDeviceType("SHUTTLE");
profile.setAdapterType("FT546_LEGACY_SRGV");
profile.setProfileVersion(1L);
profile.setCapabilities(capabilities("POINT_TO_POINT_COMMAND",
"TWO_STAGE_ATOMIC_TASK"));
Map<String, String> registers =
new LinkedHashMap<String, String>();
registers.put("taskNo", "4000");
registers.put("taskType", "4002");
registers.put("path", "4004");
registers.put("trigger", "4021");
registers.put("crossFloorFlag", "4020");
registers.put("crossFloorTarget", "4022");
registers.put("secondTaskNo", "4400");
registers.put("secondTaskType", "4402");
registers.put("secondPath", "4404");
registers.put("secondStageFlag", "4422");
registers.put("secondComplete", "4423");
profile.setRegisters(registers);
return profile;
}
private static Set<String> capabilities(String... values) {
return new LinkedHashSet<String>(Arrays.asList(values));
}
private static List<Integer> points(Integer... values) {
return Arrays.asList(values);
}
private static ExpectedWrite write(int sequence, String address,
Ft546RegisterWrite.DataType dataType,
boolean trigger,
Integer... values) {
return new ExpectedWrite(sequence, address, dataType, trigger,
Arrays.asList(values));
}
private static void assertWrites(Ft546EncodedCommand command,
ExpectedWrite... expected) {
assertEquals(expected.length, command.getWrites().size());
for (int index = 0; index < expected.length; index++) {
ExpectedWrite wanted = expected[index];
Ft546RegisterWrite actual = command.getWrites().get(index);
assertEquals(wanted.sequence, actual.getWriteSequence());
assertEquals(wanted.address, actual.getAddress());
assertEquals(wanted.dataType, actual.getDataType());
assertEquals(wanted.values, actual.getValues());
assertEquals(wanted.trigger, actual.isTrigger());
}
}
private static final class ExpectedWrite {
private final int sequence;
private final String address;
private final Ft546RegisterWrite.DataType dataType;
private final boolean trigger;
private final List<Integer> values;
private ExpectedWrite(int sequence, String address,
Ft546RegisterWrite.DataType dataType,
boolean trigger, List<Integer> values) {
this.sequence = sequence;
this.address = address;
this.dataType = dataType;
this.trigger = trigger;
this.values = values;
}
}
}

View File

@@ -0,0 +1,16 @@
{
"ordinary": [
{"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]}
],
"crossFloor": [
{"address": "4004", "kind": "INT32", "values": [70501, 70502, 0, 0, 0, 0, 0, 0]},
{"address": "4000", "kind": "INT32", "values": [30001]},
{"address": "4002", "kind": "INT16", "values": [8]},
{"address": "4021", "kind": "INT16", "values": [1]},
{"address": "4020", "kind": "INT16", "values": [1]},
{"address": "4022", "kind": "INT16", "values": [8]}
]
}

View File

@@ -0,0 +1,15 @@
{
"stageOne": [
{"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": [30001]}
],
"stageTwo": [
{"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": [30001]}
]
}