Browse Source

Merge branch 'feature/modbus-extended-functionality' of github.com:maxunbearable/thingsboard into improvement/gateway-dashboard

pull/11251/head
Vladyslav_Prykhodko 2 years ago
parent
commit
77780cd36a
  1. 325
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java
  2. 134
      common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java
  3. 2
      ui-ngx/src/app/modules/home/components/table/table-datasource.abstract.ts
  4. 4
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts
  5. 4
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts
  6. 16
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts
  7. 23
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html
  8. 110
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts
  9. 12
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html
  10. 21
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.scss
  11. 55
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts
  12. 68
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts
  13. 403
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html
  14. 28
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.scss
  15. 199
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts
  16. 222
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html
  17. 189
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts
  18. 16
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.html
  19. 147
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.ts
  20. 23
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/public-api.ts
  21. 18
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts
  22. 4
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts
  23. 22
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-ua-basic-config/opc-ua-basic-config.component.ts
  24. 27
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/public-api.ts
  25. 4
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts
  26. 77
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts
  27. 81
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  28. 0
      ui-ngx/src/app/modules/home/pipes/gateway-help-link.pipe.ts
  29. 14
      ui-ngx/src/app/modules/home/pipes/gateway-port-tooltip.pipe.ts
  30. 18
      ui-ngx/src/app/modules/home/pipes/public-api.ts
  31. 17
      ui-ngx/src/app/shared/abstract/public-api.ts
  32. 5
      ui-ngx/src/app/shared/directives/ellipsis-chip-list.directive.ts
  33. 15
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  34. 3
      ui-ngx/src/assets/metadata/connector-default-configs/modbus.json

325
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java

@ -61,6 +61,7 @@ public class TbUtils {
private static final int HEX_LEN_MIN = -1;
private static final int HEX_LEN_INT_MAX = 8;
private static final int HEX_LEN_LONG_MAX = 16;
private static final int BYTES_LEN_INT_MAX = 4;
private static final int BYTES_LEN_LONG_MAX = 8;
private static final LinkedHashMap<String, String> mdnEncodingReplacements = new LinkedHashMap<>();
@ -117,6 +118,8 @@ public class TbUtils {
String.class)));
parserConfig.addImport("parseFloat", new MethodStub(TbUtils.class.getMethod("parseFloat",
String.class, int.class)));
parserConfig.addImport("parseHexIntLongToFloat", new MethodStub(TbUtils.class.getMethod("parseHexIntLongToFloat",
String.class, boolean.class)));
parserConfig.addImport("parseDouble", new MethodStub(TbUtils.class.getMethod("parseDouble",
String.class)));
parserConfig.addImport("parseLittleEndianHexToInt", new MethodStub(TbUtils.class.getMethod("parseLittleEndianHexToInt",
@ -127,10 +130,18 @@ public class TbUtils {
String.class)));
parserConfig.addImport("parseHexToInt", new MethodStub(TbUtils.class.getMethod("parseHexToInt",
String.class, boolean.class)));
parserConfig.addImport("parseBytesToInt", new MethodStub(TbUtils.class.getMethod("parseBytesToInt",
List.class)));
parserConfig.addImport("parseBytesToInt", new MethodStub(TbUtils.class.getMethod("parseBytesToInt",
List.class, int.class)));
parserConfig.addImport("parseBytesToInt", new MethodStub(TbUtils.class.getMethod("parseBytesToInt",
List.class, int.class, int.class)));
parserConfig.addImport("parseBytesToInt", new MethodStub(TbUtils.class.getMethod("parseBytesToInt",
List.class, int.class, int.class, boolean.class)));
parserConfig.addImport("parseBytesToInt", new MethodStub(TbUtils.class.getMethod("parseBytesToInt",
byte[].class)));
parserConfig.addImport("parseBytesToInt", new MethodStub(TbUtils.class.getMethod("parseBytesToInt",
byte[].class, int.class)));
parserConfig.addImport("parseBytesToInt", new MethodStub(TbUtils.class.getMethod("parseBytesToInt",
byte[].class, int.class, int.class)));
parserConfig.addImport("parseBytesToInt", new MethodStub(TbUtils.class.getMethod("parseBytesToInt",
@ -143,10 +154,18 @@ public class TbUtils {
String.class)));
parserConfig.addImport("parseHexToLong", new MethodStub(TbUtils.class.getMethod("parseHexToLong",
String.class, boolean.class)));
parserConfig.addImport("parseBytesToLong", new MethodStub(TbUtils.class.getMethod("parseBytesToLong",
List.class)));
parserConfig.addImport("parseBytesToLong", new MethodStub(TbUtils.class.getMethod("parseBytesToLong",
List.class, int.class)));
parserConfig.addImport("parseBytesToLong", new MethodStub(TbUtils.class.getMethod("parseBytesToLong",
List.class, int.class, int.class)));
parserConfig.addImport("parseBytesToLong", new MethodStub(TbUtils.class.getMethod("parseBytesToLong",
List.class, int.class, int.class, boolean.class)));
parserConfig.addImport("parseBytesToLong", new MethodStub(TbUtils.class.getMethod("parseBytesToLong",
byte[].class)));
parserConfig.addImport("parseBytesToLong", new MethodStub(TbUtils.class.getMethod("parseBytesToLong",
byte[].class, int.class)));
parserConfig.addImport("parseBytesToLong", new MethodStub(TbUtils.class.getMethod("parseBytesToLong",
byte[].class, int.class, int.class)));
parserConfig.addImport("parseBytesToLong", new MethodStub(TbUtils.class.getMethod("parseBytesToLong",
@ -160,13 +179,21 @@ public class TbUtils {
parserConfig.addImport("parseHexToFloat", new MethodStub(TbUtils.class.getMethod("parseHexToFloat",
String.class, boolean.class)));
parserConfig.addImport("parseBytesToFloat", new MethodStub(TbUtils.class.getMethod("parseBytesToFloat",
byte[].class, int.class, boolean.class)));
List.class)));
parserConfig.addImport("parseBytesToFloat", new MethodStub(TbUtils.class.getMethod("parseBytesToFloat",
List.class, int.class)));
parserConfig.addImport("parseBytesToFloat", new MethodStub(TbUtils.class.getMethod("parseBytesToFloat",
List.class, int.class, int.class)));
parserConfig.addImport("parseBytesToFloat", new MethodStub(TbUtils.class.getMethod("parseBytesToFloat",
List.class, int.class, int.class, boolean.class)));
parserConfig.addImport("parseBytesToFloat", new MethodStub(TbUtils.class.getMethod("parseBytesToFloat",
byte[].class)));
parserConfig.addImport("parseBytesToFloat", new MethodStub(TbUtils.class.getMethod("parseBytesToFloat",
byte[].class, int.class)));
parserConfig.addImport("parseBytesToFloat", new MethodStub(TbUtils.class.getMethod("parseBytesToFloat",
List.class, int.class, boolean.class)));
byte[].class, int.class, int.class)));
parserConfig.addImport("parseBytesToFloat", new MethodStub(TbUtils.class.getMethod("parseBytesToFloat",
List.class, int.class)));
byte[].class, int.class, int.class, boolean.class)));
parserConfig.addImport("parseLittleEndianHexToDouble", new MethodStub(TbUtils.class.getMethod("parseLittleEndianHexToDouble",
String.class)));
parserConfig.addImport("parseBigEndianHexToDouble", new MethodStub(TbUtils.class.getMethod("parseBigEndianHexToDouble",
@ -176,13 +203,21 @@ public class TbUtils {
parserConfig.addImport("parseHexToDouble", new MethodStub(TbUtils.class.getMethod("parseHexToDouble",
String.class, boolean.class)));
parserConfig.addImport("parseBytesToDouble", new MethodStub(TbUtils.class.getMethod("parseBytesToDouble",
byte[].class, int.class)));
parserConfig.addImport("parseBytesToDouble", new MethodStub(TbUtils.class.getMethod("parseBytesToDouble",
byte[].class, int.class, boolean.class)));
List.class)));
parserConfig.addImport("parseBytesToDouble", new MethodStub(TbUtils.class.getMethod("parseBytesToDouble",
List.class, int.class)));
parserConfig.addImport("parseBytesToDouble", new MethodStub(TbUtils.class.getMethod("parseBytesToDouble",
List.class, int.class, boolean.class)));
List.class, int.class, int.class)));
parserConfig.addImport("parseBytesToDouble", new MethodStub(TbUtils.class.getMethod("parseBytesToDouble",
List.class, int.class, int.class, boolean.class)));
parserConfig.addImport("parseBytesToDouble", new MethodStub(TbUtils.class.getMethod("parseBytesToDouble",
byte[].class)));
parserConfig.addImport("parseBytesToDouble", new MethodStub(TbUtils.class.getMethod("parseBytesToDouble",
byte[].class, int.class)));
parserConfig.addImport("parseBytesToDouble", new MethodStub(TbUtils.class.getMethod("parseBytesToDouble",
byte[].class, int.class, int.class)));
parserConfig.addImport("parseBytesToDouble", new MethodStub(TbUtils.class.getMethod("parseBytesToDouble",
byte[].class, int.class, int.class, boolean.class)));
parserConfig.addImport("toFixed", new MethodStub(TbUtils.class.getMethod("toFixed",
double.class, int.class)));
parserConfig.addImport("toFixed", new MethodStub(TbUtils.class.getMethod("toFixed",
@ -201,17 +236,17 @@ public class TbUtils {
Long.class)));
parserConfig.addImport("longToHex", new MethodStub(TbUtils.class.getMethod("longToHex",
Long.class, boolean.class)));
parserConfig.addImport("longToHex", new MethodStub(TbUtils.class.getMethod("longToHex",
parserConfig.addImport("longToHex", new MethodStub(TbUtils.class.getMethod("longToHex",
Long.class, boolean.class, boolean.class)));
parserConfig.addImport("longToHex", new MethodStub(TbUtils.class.getMethod("longToHex",
parserConfig.addImport("longToHex", new MethodStub(TbUtils.class.getMethod("longToHex",
Long.class, boolean.class, boolean.class, int.class)));
parserConfig.addImport("intLongToString", new MethodStub(TbUtils.class.getMethod("intLongToString",
parserConfig.addImport("intLongToString", new MethodStub(TbUtils.class.getMethod("intLongToString",
Long.class)));
parserConfig.addImport("intLongToString", new MethodStub(TbUtils.class.getMethod("intLongToString",
parserConfig.addImport("intLongToString", new MethodStub(TbUtils.class.getMethod("intLongToString",
Long.class, int.class)));
parserConfig.addImport("intLongToString", new MethodStub(TbUtils.class.getMethod("intLongToString",
parserConfig.addImport("intLongToString", new MethodStub(TbUtils.class.getMethod("intLongToString",
Long.class, int.class, boolean.class)));
parserConfig.addImport("intLongToString", new MethodStub(TbUtils.class.getMethod("intLongToString",
parserConfig.addImport("intLongToString", new MethodStub(TbUtils.class.getMethod("intLongToString",
Long.class, int.class, boolean.class, boolean.class)));
parserConfig.addImport("floatToHex", new MethodStub(TbUtils.class.getMethod("floatToHex",
Float.class)));
@ -334,10 +369,14 @@ public class TbUtils {
}
public static Integer parseInt(String value, int radix) {
if (StringUtils.isNotBlank(value)) {
String valueP = prepareNumberString(value);
return parseInt(value, radix, true);
}
private static Integer parseInt(String value, int radix, boolean bigEndian) {
String valueP = prepareNumberString(value, bigEndian);
if (valueP != null) {
int radixValue = isValidStringAndRadix(valueP, radix, value);
if (radixValue >= 25 && radixValue <= MAX_RADIX) {
if (radixValue >= 25 && radixValue <= MAX_RADIX) {
return (Integer) compareIntLongValueMinMax(valueP, radixValue, Integer.MAX_VALUE, Integer.MIN_VALUE);
}
return switch (radixValue) {
@ -354,10 +393,14 @@ public class TbUtils {
}
public static Long parseLong(String value, int radix) {
if (StringUtils.isNotBlank(value)) {
String valueP = prepareNumberString(value);
return parseLong(value, radix, true);
}
private static Long parseLong(String value, int radix, boolean bigEndian) {
String valueP = prepareNumberString(value, bigEndian);
if (valueP != null) {
int radixValue = isValidStringAndRadix(valueP, radix, value);
if (radixValue >= 25 && radixValue <= MAX_RADIX) {
if (radixValue >= 25 && radixValue <= MAX_RADIX) {
return (Long) compareIntLongValueMinMax(valueP, radixValue, Long.MAX_VALUE, Long.MIN_VALUE);
}
return switch (radixValue) {
@ -385,6 +428,7 @@ public class TbUtils {
return Integer.parseInt(binaryString, MIN_RADIX);
}
}
private static long parseBinaryStringAsSignedLong(String binaryString) {
if (binaryString.length() != 64) {
// Pad the binary string to 64 bits if it is not already
@ -420,34 +464,58 @@ public class TbUtils {
}
public static Float parseFloat(String value) {
return parseFloat(value, 0);
return parseFloat(value, ZERO_RADIX);
}
public static Float parseFloat(String value, int radix) {
if (StringUtils.isNotBlank(value)) {
String valueP = prepareNumberString(value);
int radixValue = isValidStringAndRadix(valueP, radix, value);
if (radixValue == DEC_RADIX) {
return Float.parseFloat(value);
} else {
int bits = Integer.parseUnsignedInt(valueP, HEX_RADIX);
return Float.intBitsToFloat(bits);
String valueP = prepareNumberString(value, true);
if (valueP != null) {
return parseFloatFromString(value, valueP, radix);
}
return null;
}
private static Float parseFloatFromString(String value, String valueP, int radix) {
int radixValue = isValidStringAndRadix(valueP, radix, value);
if (radixValue == HEX_RADIX) {
int bits = (int) Long.parseLong(valueP, HEX_RADIX);
// Hex representation is a standard IEEE 754 float value (eg "0x41200000" for 10.0f).
return Float.intBitsToFloat(bits);
} else {
return Float.parseFloat(value);
}
}
public static Float parseHexIntLongToFloat(String value, boolean bigEndian) {
String valueP = prepareNumberString(value, bigEndian);
if (valueP != null) {
int radixValue = isValidStringAndRadix(valueP, HEX_RADIX, value);
if (radixValue == HEX_RADIX) {
int bits = (int) Long.parseLong(valueP, HEX_RADIX);
// If the length is not equal to 8 characters, we process it as an integer (eg "0x0A" for 10.0f).
float floatValue = (float) bits;
return Float.valueOf(floatValue);
}
}
return null;
}
public static Double parseDouble(String value) {
int radix = getRadix10_16(value);
return parseDouble(value, radix);
}
public static Double parseDouble(String value, int radix) {
if (value != null) {
String valueP = prepareNumberString(value);
return parseDouble(value, radix, true);
}
private static Double parseDouble(String value, int radix, boolean bigEndian) {
String valueP = prepareNumberString(value, bigEndian);
if (valueP != null) {
int radixValue = isValidStringAndRadix(valueP, radix, value);
if (radixValue == DEC_RADIX) {
return Double.parseDouble(prepareNumberString(value));
return Double.parseDouble(valueP);
} else {
long bits = Long.parseUnsignedLong(valueP, HEX_RADIX);
return Double.longBitsToDouble(bits);
@ -469,9 +537,7 @@ public class TbUtils {
}
public static Integer parseHexToInt(String value, boolean bigEndian) {
String hexValue = prepareNumberString(value);
String hex = bigEndian ? hexValue : reverseHexStringByOrder(hexValue);
return parseInt(hex, HEX_RADIX);
return parseInt(value, HEX_RADIX, bigEndian);
}
public static long parseLittleEndianHexToLong(String hex) {
@ -487,9 +553,7 @@ public class TbUtils {
}
public static Long parseHexToLong(String value, boolean bigEndian) {
String hexValue = prepareNumberString(value);
String hex = bigEndian ? value : reverseHexStringByOrder(hexValue);
return parseLong(hex, HEX_RADIX);
return parseLong(value, HEX_RADIX, bigEndian);
}
public static float parseLittleEndianHexToFloat(String hex) {
@ -505,9 +569,11 @@ public class TbUtils {
}
public static Float parseHexToFloat(String value, boolean bigEndian) {
String hexValue = prepareNumberString(value);
String hex = bigEndian ? value : reverseHexStringByOrder(hexValue);
return parseFloat(hex, HEX_RADIX);
String valueP = prepareNumberString(value, bigEndian);
if (valueP != null) {
return parseFloatFromString(value, valueP, HEX_RADIX);
}
return null;
}
public static double parseLittleEndianHexToDouble(String hex) {
@ -523,13 +589,11 @@ public class TbUtils {
}
public static double parseHexToDouble(String value, boolean bigEndian) {
String hexValue = prepareNumberString(value);
String hex = bigEndian ? value : reverseHexStringByOrder(hexValue);
return parseDouble(hex, HEX_RADIX);
return parseDouble(value, HEX_RADIX, bigEndian);
}
public static ExecutionArrayList<Byte> hexToBytes(ExecutionContext ctx, String value) {
String hex = prepareNumberString(value);
String hex = prepareNumberString(value, true);
int len = hex.length();
if (len % 2 > 0) {
throw new IllegalArgumentException("Hex string must be even-length.");
@ -575,6 +639,7 @@ public class TbUtils {
public static String longToHex(Long l) {
return prepareNumberHexString(l, true, false, HEX_LEN_MIN, HEX_LEN_LONG_MAX);
}
public static String longToHex(Long l, boolean bigEndian) {
return prepareNumberHexString(l, bigEndian, false, HEX_LEN_MIN, HEX_LEN_LONG_MAX);
}
@ -604,7 +669,7 @@ public class TbUtils {
return Long.toString(number, radix);
}
return switch (radix) {
case MIN_RADIX -> Long.toBinaryString(number);
case MIN_RADIX -> Long.toBinaryString(number);
case OCTAL_RADIX -> Long.toOctalString(number);
case DEC_RADIX -> Long.toString(number);
case HEX_RADIX -> prepareNumberHexString(number, bigEndian, pref, -1, -1);
@ -646,13 +711,13 @@ public class TbUtils {
private static String removeLeadingZero_FF(String hex, Long number, int hexLenMax) {
String hexWithoutZero = hex.replaceFirst("^0+(?!$)", ""); // Remove leading zeros except for the last one
hexWithoutZero = hexWithoutZero.length() % 2 > 0 ? "0" + hexWithoutZero : hexWithoutZero;
hexWithoutZero = hexWithoutZero.length() % 2 > 0 ? "0" + hexWithoutZero : hexWithoutZero;
if (number >= 0) {
return hexWithoutZero;
} else {
String hexWithoutZeroFF = hexWithoutZero.replaceFirst("^F+(?!$)", "");
hexWithoutZeroFF = hexWithoutZeroFF.length() % 2 > 0 ? "F" + hexWithoutZeroFF : hexWithoutZeroFF;
if (hexWithoutZeroFF.length() > hexLenMax) {
hexWithoutZeroFF = hexWithoutZeroFF.length() % 2 > 0 ? "F" + hexWithoutZeroFF : hexWithoutZeroFF;
if (hexWithoutZeroFF.length() > hexLenMax) {
return hexWithoutZeroFF.substring(hexWithoutZeroFF.length() - hexLenMax);
} else if (hexWithoutZeroFF.length() == hexLenMax) {
return hexWithoutZeroFF;
@ -668,7 +733,7 @@ public class TbUtils {
public static String floatToHex(Float f, boolean bigEndian) {
// Convert the float to its raw integer bits representation
int bits = Float.floatToRawIntBits(f);
int bits = Float.floatToIntBits(f);
// Format the integer bits as a hexadecimal string
String result = String.format("0x%08X", bits);
@ -699,16 +764,28 @@ public class TbUtils {
return Base64.getDecoder().decode(input);
}
public static int parseBytesToInt(List<Byte> data) {
return parseBytesToInt(Bytes.toArray(data));
}
public static int parseBytesToInt(List<Byte> data, int offset) {
return parseBytesToInt(Bytes.toArray(data), offset);
}
public static int parseBytesToInt(List<Byte> data, int offset, int length) {
return parseBytesToInt(data, offset, length, true);
return parseBytesToInt(Bytes.toArray(data), offset, length);
}
public static int parseBytesToInt(List<Byte> data, int offset, int length, boolean bigEndian) {
final byte[] bytes = new byte[data.size()];
for (int i = 0; i < bytes.length; i++) {
bytes[i] = data.get(i);
}
return parseBytesToInt(bytes, offset, length, bigEndian);
return parseBytesToInt(Bytes.toArray(data), offset, length, bigEndian);
}
public static int parseBytesToInt(byte[] data) {
return parseBytesToInt(data, 0);
}
public static int parseBytesToInt(byte[] data, int offset) {
return parseBytesToInt(data, offset, BYTES_LEN_INT_MAX);
}
public static int parseBytesToInt(byte[] data, int offset, int length) {
@ -719,7 +796,7 @@ public class TbUtils {
if (offset > data.length) {
throw new IllegalArgumentException("Offset: " + offset + " is out of bounds for array with length: " + data.length + "!");
}
if (length > 4) {
if (length > BYTES_LEN_INT_MAX) {
throw new IllegalArgumentException("Length: " + length + " is too large. Maximum 4 bytes is allowed!");
}
if (offset + length > data.length) {
@ -735,16 +812,28 @@ public class TbUtils {
return bb.getInt();
}
public static long parseBytesToLong(List<Byte> data) {
return parseBytesToLong(Bytes.toArray(data));
}
public static long parseBytesToLong(List<Byte> data, int offset) {
return parseBytesToLong(Bytes.toArray(data), offset);
}
public static long parseBytesToLong(List<Byte> data, int offset, int length) {
return parseBytesToLong(data, offset, length, true);
return parseBytesToLong(Bytes.toArray(data), offset, length);
}
public static long parseBytesToLong(List<Byte> data, int offset, int length, boolean bigEndian) {
final byte[] bytes = new byte[data.size()];
for (int i = 0; i < bytes.length; i++) {
bytes[i] = data.get(i);
}
return parseBytesToLong(bytes, offset, length, bigEndian);
return parseBytesToLong(Bytes.toArray(data), offset, length, bigEndian);
}
public static long parseBytesToLong(byte[] data) {
return parseBytesToLong(data, 0);
}
public static long parseBytesToLong(byte[] data, int offset) {
return parseBytesToLong(data, offset, BYTES_LEN_LONG_MAX);
}
public static long parseBytesToLong(byte[] data, int offset, int length) {
@ -761,49 +850,118 @@ public class TbUtils {
if (offset + length > data.length) {
throw new IllegalArgumentException("Offset: " + offset + " and Length: " + length + " is out of bounds for array with length: " + data.length + "!");
}
var bb = ByteBuffer.allocate(8);
var bb = ByteBuffer.allocate(BYTES_LEN_LONG_MAX);
if (!bigEndian) {
bb.order(ByteOrder.LITTLE_ENDIAN);
}
bb.position(bigEndian ? 8 - length : 0);
bb.position(bigEndian ? BYTES_LEN_LONG_MAX - length : 0);
bb.put(data, offset, length);
bb.position(0);
return bb.getLong();
}
public static float parseBytesToFloat(byte[] data, int offset) {
return parseBytesToFloat(data, offset, true);
public static float parseBytesToFloat(List data) {
return parseBytesToFloat(Bytes.toArray(data), 0);
}
public static float parseBytesToFloat(List data, int offset) {
return parseBytesToFloat(data, offset, true);
return parseBytesToFloat(Bytes.toArray(data), offset, BYTES_LEN_INT_MAX);
}
public static float parseBytesToFloat(List data, int offset, boolean bigEndian) {
return parseBytesToFloat(Bytes.toArray(data), offset, bigEndian);
public static float parseBytesToFloat(List data, int offset, int length) {
return parseBytesToFloat(Bytes.toArray(data), offset, length, true);
}
public static float parseBytesToFloat(byte[] data, int offset, boolean bigEndian) {
byte[] bytesToNumber = prepareBytesToNumber(data, offset, 4, bigEndian);
return ByteBuffer.wrap(bytesToNumber).getFloat();
public static float parseBytesToFloat(List data, int offset, int length, boolean bigEndian) {
return parseBytesToFloat(Bytes.toArray(data), offset, length, bigEndian);
}
public static float parseBytesToFloat(byte[] data) {
return parseBytesToFloat(data, 0);
}
public static double parseBytesToDouble(byte[] data, int offset) {
return parseBytesToDouble(data, offset, true);
public static float parseBytesToFloat(byte[] data, int offset) {
return parseBytesToFloat(data, offset, BYTES_LEN_INT_MAX);
}
public static float parseBytesToFloat(byte[] data, int offset, int length) {
return parseBytesToFloat(data, offset, length, true);
}
public static float parseBytesToFloat(byte[] data, int offset, int length, boolean bigEndian) {
if (length > BYTES_LEN_INT_MAX) {
throw new IllegalArgumentException("Length: " + length + " is too large. Maximum " + BYTES_LEN_INT_MAX + " bytes is allowed!");
}
if (offset + length > data.length) {
throw new IllegalArgumentException("Offset: " + offset + " and Length: " + length + " is out of bounds for array with length: " + data.length + "!");
}
byte[] bytesToNumber = prepareBytesToNumber(data, offset, length, bigEndian);
if (bytesToNumber.length < BYTES_LEN_INT_MAX) {
byte[] extendedBytes = new byte[BYTES_LEN_INT_MAX];
Arrays.fill(extendedBytes, (byte) 0);
System.arraycopy(bytesToNumber, 0, extendedBytes, 0, bytesToNumber.length);
bytesToNumber = extendedBytes;
}
float floatValue = ByteBuffer.wrap(bytesToNumber).getFloat();
if (!Float.isNaN(floatValue)) {
return floatValue;
} else {
long longValue = parseBytesToLong(bytesToNumber, 0, BYTES_LEN_INT_MAX);
BigDecimal bigDecimalValue = new BigDecimal(longValue);
return bigDecimalValue.floatValue();
}
}
public static double parseBytesToDouble(List data) {
return parseBytesToDouble(Bytes.toArray(data));
}
public static double parseBytesToDouble(List data, int offset) {
return parseBytesToDouble(data, offset, true);
return parseBytesToDouble(Bytes.toArray(data), offset);
}
public static double parseBytesToDouble(List data, int offset, int length) {
return parseBytesToDouble(Bytes.toArray(data), offset, length);
}
public static double parseBytesToDouble(List data, int offset, int length, boolean bigEndian) {
return parseBytesToDouble(Bytes.toArray(data), offset, length, bigEndian);
}
public static double parseBytesToDouble(List data, int offset, boolean bigEndian) {
return parseBytesToDouble(Bytes.toArray(data), offset, bigEndian);
public static double parseBytesToDouble(byte[] data) {
return parseBytesToDouble(data, 0);
}
public static double parseBytesToDouble(byte[] data, int offset, boolean bigEndian) {
byte[] bytesToNumber = prepareBytesToNumber(data, offset, BYTES_LEN_LONG_MAX, bigEndian);
return ByteBuffer.wrap(bytesToNumber).getDouble();
public static double parseBytesToDouble(byte[] data, int offset) {
return parseBytesToDouble(data, offset, BYTES_LEN_LONG_MAX);
}
public static double parseBytesToDouble(byte[] data, int offset, int length) {
return parseBytesToDouble(data, offset, length, true);
}
public static double parseBytesToDouble(byte[] data, int offset, int length, boolean bigEndian) {
if (length > BYTES_LEN_LONG_MAX) {
throw new IllegalArgumentException("Length: " + length + " is too large. Maximum " + BYTES_LEN_LONG_MAX + " bytes is allowed!");
}
if (offset + length > data.length) {
throw new IllegalArgumentException("Offset: " + offset + " and Length: " + length + " is out of bounds for array with length: " + data.length + "!");
}
byte[] bytesToNumber = prepareBytesToNumber(data, offset, length, bigEndian);
if (bytesToNumber.length < BYTES_LEN_LONG_MAX) {
byte[] extendedBytes = new byte[BYTES_LEN_LONG_MAX];
Arrays.fill(extendedBytes, (byte) 0);
System.arraycopy(bytesToNumber, 0, extendedBytes, 0, bytesToNumber.length);
bytesToNumber = extendedBytes;
}
double doubleValue = ByteBuffer.wrap(bytesToNumber).getDouble();
if (!Double.isNaN(doubleValue)) {
return doubleValue;
} else {
BigInteger bigInt = new BigInteger(1, bytesToNumber);
BigDecimal bigDecimalValue = new BigDecimal(bigInt);
return bigDecimalValue.doubleValue();
}
}
private static byte[] prepareBytesToNumber(byte[] data, int offset, int length, boolean bigEndian) {
@ -931,14 +1089,15 @@ public class TbUtils {
}
}
private static String prepareNumberString(String value) {
if (value != null) {
private static String prepareNumberString(String value, boolean bigEndian) {
if (StringUtils.isNotBlank(value)) {
value = value.trim();
value = value.replace("0x", "");
value = value.replace("0X", "");
value = value.replace(",", ".");
return bigEndian ? value : reverseHexStringByOrder(value);
}
return value;
return null;
}
private static int isValidStringAndRadix(String valueP, int radix, String value) {

134
common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java

@ -30,6 +30,7 @@ import org.mvel2.execution.ExecutionArrayList;
import org.mvel2.execution.ExecutionHashMap;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.ArrayList;
@ -47,7 +48,7 @@ public class TbUtilsTest {
private ExecutionContext ctx;
private final float floatVal = 29.29824f;
private final Float floatVal = 29.29824f;
private final float floatValRev = -5.948442E7f;
@ -256,7 +257,7 @@ public class TbUtilsTest {
@Test
public void parseFloat() {
String floatValStr = "29.29824";
String floatValStr = floatVal.toString();
Assertions.assertEquals(java.util.Optional.of(floatVal).get(), TbUtils.parseFloat(floatValStr));
String floatValHex = "41EA62CC";
Assertions.assertEquals(0, Float.compare(floatVal, TbUtils.parseHexToFloat(floatValHex)));
@ -274,14 +275,49 @@ public class TbUtilsTest {
}
@Test
public void arseBytesToFloat() {
public void parseBytesToFloat() {
byte[] floatValByte = {65, -22, 98, -52};
Assertions.assertEquals(0, Float.compare(floatVal, TbUtils.parseBytesToFloat(floatValByte, 0)));
Assertions.assertEquals(0, Float.compare(floatValRev, TbUtils.parseBytesToFloat(floatValByte, 0, false)));
List<Byte> floatVaList = Bytes.asList(floatValByte);
Assertions.assertEquals(0, Float.compare(floatVal, TbUtils.parseBytesToFloat(floatVaList, 0)));
Assertions.assertEquals(0, Float.compare(floatValRev, TbUtils.parseBytesToFloat(floatVaList, 0, false)));
Assertions.assertEquals(0, Float.compare(floatValRev, TbUtils.parseBytesToFloat(floatValByte, 0, 4, false)));
List<Byte> floatValList = Bytes.asList(floatValByte);
Assertions.assertEquals(0, Float.compare(floatVal, TbUtils.parseBytesToFloat(floatValList, 0)));
Assertions.assertEquals(0, Float.compare(floatValRev, TbUtils.parseBytesToFloat(floatValList, 0, 4, false)));
// 4 294 967 295L == {0xFF, 0xFF, 0xFF, 0xFF}
floatValByte = new byte[]{-1, -1, -1, -1};
float floatExpectedBe = 4294.9673f;
float floatExpectedLe = 4.2949673E9f;
float actualBe = TbUtils.parseBytesToFloat(floatValByte, 0, 4, true);
Assertions.assertEquals(0, Float.compare(floatExpectedBe, actualBe / 1000000));
Assertions.assertEquals(0, Float.compare(floatExpectedLe, TbUtils.parseBytesToFloat(floatValByte, 0, 4, false)));
floatValList = Bytes.asList(floatValByte);
actualBe = TbUtils.parseBytesToFloat(floatValList, 0);
Assertions.assertEquals(0, Float.compare(floatExpectedBe, actualBe / 1000000));
Assertions.assertEquals(0, Float.compare(floatExpectedLe, TbUtils.parseBytesToFloat(floatValList, 0, 4, false)));
// 2 143 289 344L == {0x7F, 0xC0, 0x00, 0x00}
floatValByte = new byte[]{0x7F, (byte) 0xC0, (byte) 0xFF, 0x00};
floatExpectedBe = 2143.3547f;
floatExpectedLe = -3.984375f;
actualBe = TbUtils.parseBytesToFloat(floatValByte, 0, 4, true);
Assertions.assertEquals(0, Float.compare(floatExpectedBe, actualBe / 1000000));
Assertions.assertEquals(0, Float.compare(floatExpectedLe, TbUtils.parseBytesToFloat(floatValByte, 0, 2, false)));
floatValList = Bytes.asList(floatValByte);
floatExpectedLe = 4.2908055E9f;
actualBe = TbUtils.parseBytesToFloat(floatValList, 0);
Assertions.assertEquals(0, Float.compare(floatExpectedBe, actualBe / 1000000));
Assertions.assertEquals(0, Float.compare(floatExpectedLe, TbUtils.parseBytesToFloat(floatValList, 0, 3, false)));
// "01752B0367FA000500010488 FFFFFFFF FFFFFFFF 33";
String intToHexBe = "01752B0367FA000500010488FFFFFFFFFFFFFFFF33";
floatExpectedLe = 4294.9673f;
floatValList = TbUtils.hexToBytes(ctx, intToHexBe);
float actualLe = TbUtils.parseBytesToFloat(floatValList, 12, 4, false);
Assertions.assertEquals(0, Float.compare(floatExpectedLe, actualLe / 1000000));
actualLe = TbUtils.parseBytesToFloat(floatValList, 12 + 4, 4, false);
Assertions.assertEquals(0, Float.compare(floatExpectedLe, actualLe / 1000000));
}
@Test
@ -356,11 +392,40 @@ public class TbUtilsTest {
public void parseBytesToDouble() {
byte[] doubleValByte = {64, -101, 4, -79, 12, -78, -107, -22};
Assertions.assertEquals(0, Double.compare(doubleVal, TbUtils.parseBytesToDouble(doubleValByte, 0)));
Assertions.assertEquals(0, Double.compare(doubleValRev, TbUtils.parseBytesToDouble(doubleValByte, 0, false)));
List<Byte> doubleVaList = Bytes.asList(doubleValByte);
Assertions.assertEquals(0, Double.compare(doubleVal, TbUtils.parseBytesToDouble(doubleVaList, 0)));
Assertions.assertEquals(0, Double.compare(doubleValRev, TbUtils.parseBytesToDouble(doubleVaList, 0, false)));
Assertions.assertEquals(0, Double.compare(doubleValRev, TbUtils.parseBytesToDouble(doubleValByte, 0, 8, false)));
List<Byte> doubleValList = Bytes.asList(doubleValByte);
Assertions.assertEquals(0, Double.compare(doubleVal, TbUtils.parseBytesToDouble(doubleValList, 0)));
Assertions.assertEquals(0, Double.compare(doubleValRev, TbUtils.parseBytesToDouble(doubleValList, 0, 8, false)));
// 4 294 967 295L == {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}
doubleValByte = new byte[]{-1, -1, -1, -1, -1, -1, -1, -1};
double doubleExpectedBe = 18446.744073709553d;
double doubleExpectedLe = 1.8446744073709552E19d;
double actualBe = TbUtils.parseBytesToDouble(doubleValByte, 0, 8, true);
Assertions.assertEquals(0, Double.compare(doubleExpectedBe, actualBe / 1000000000000000L));
Assertions.assertEquals(0, Double.compare(doubleExpectedLe, TbUtils.parseBytesToDouble(doubleValByte, 0, 8, false)));
doubleValList = Bytes.asList(doubleValByte);
Assertions.assertEquals(0, Double.compare(doubleExpectedBe, TbUtils.parseBytesToDouble(doubleValList, 0) / 1000000000000000L));
Assertions.assertEquals(0, Double.compare(doubleExpectedLe, TbUtils.parseBytesToDouble(doubleValList, 0, 8, false)));
doubleValByte = new byte[]{0x7F, (byte) 0xC0, (byte) 0xFF, 0x00, 0x7F, (byte) 0xC0, (byte) 0xFF, 0x00};
doubleExpectedBe = 2387013.651780523d;
doubleExpectedLe = 7.234601680440024E-304d;
actualBe = TbUtils.parseBytesToDouble(doubleValByte, 0, 8, true);
BigDecimal bigDecimal = new BigDecimal(actualBe);
// We move the decimal point to the left by 301 positions
actualBe = bigDecimal.movePointLeft(301).doubleValue();
Assertions.assertEquals(0, Double.compare(doubleExpectedBe, actualBe));
Assertions.assertEquals(0, Double.compare(doubleExpectedLe, TbUtils.parseBytesToDouble(doubleValByte, 0, 8, false)));
doubleValList = Bytes.asList(doubleValByte);
doubleExpectedLe = 5.828674572203954E303d;
actualBe = TbUtils.parseBytesToDouble(doubleValList, 0);
bigDecimal = new BigDecimal(actualBe);
actualBe = bigDecimal.movePointLeft(301).doubleValue();
Assertions.assertEquals(0, Double.compare(doubleExpectedBe, actualBe));
Assertions.assertEquals(0, Double.compare(doubleExpectedLe, TbUtils.parseBytesToDouble(doubleValList, 0, 5, false)));
}
@Test
@ -605,15 +670,52 @@ public class TbUtilsTest {
@Test
public void floatToHex_Test() {
Float value = 20.89f;
String expectedHex = "0x41A71EB8";
String valueHexRev = "0xB81EA741";
Float value = 123456789.00f;
String expectedHex = "0x4CEB79A3";
String valueHexRev = "0xA379EB4C";
String actual = TbUtils.floatToHex(value);
Assertions.assertEquals(expectedHex, actual);
Float valueActual = TbUtils.parseHexToFloat(actual);
Assertions.assertEquals(value, valueActual);
valueActual = TbUtils.parseHexToFloat(valueHexRev, false);
Assertions.assertEquals(value, valueActual);
value = 123456789.67f;
expectedHex = "0x4CEB79A3";
valueHexRev = "0xA379EB4C";
actual = TbUtils.floatToHex(value);
Assertions.assertEquals(expectedHex, actual);
valueActual = TbUtils.parseHexToFloat(actual);
Assertions.assertEquals(value, valueActual);
valueActual = TbUtils.parseHexToFloat(valueHexRev, false);
Assertions.assertEquals(value, valueActual);
value = 10.0f;
expectedHex = "0x41200000";
valueHexRev = "0x00002041";
actual = TbUtils.floatToHex(value);
Assertions.assertEquals(expectedHex, actual);
valueActual = TbUtils.parseHexToFloat(actual);
Assertions.assertEquals(value, valueActual);
valueActual = TbUtils.parseHexToFloat(valueHexRev, false);
Assertions.assertEquals(value, valueActual);
}
// If the length is not equal to 8 characters, we process it as an integer (eg "0x0A" for 10.0f).
@Test
public void parseHexIntLongToFloat_Test() {
Float valueExpected = 10.0f;
Float valueActual = TbUtils.parseHexIntLongToFloat("0x0A", true);
Assertions.assertEquals(valueExpected, valueActual);
valueActual = TbUtils.parseHexIntLongToFloat("0x0A", false);
Assertions.assertEquals(valueExpected, valueActual);
valueActual = TbUtils.parseHexIntLongToFloat("0x00000A", true);
Assertions.assertEquals(valueExpected, valueActual);
valueActual = TbUtils.parseHexIntLongToFloat("0x0A0000", false);
Assertions.assertEquals(valueExpected, valueActual);
valueExpected = 2570.0f;
valueActual = TbUtils.parseHexIntLongToFloat("0x000A0A", true);
Assertions.assertEquals(valueExpected, valueActual);
valueActual = TbUtils.parseHexIntLongToFloat("0x0A0A00", false);
Assertions.assertEquals(valueExpected, valueActual);
}
@Test

2
ui-ngx/src/app/shared/abstract/datasource/datasource.abstract.ts → ui-ngx/src/app/modules/home/components/table/table-datasource.abstract.ts

@ -18,7 +18,7 @@ import { DataSource } from '@angular/cdk/collections';
import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export abstract class TbDatasource<T> implements DataSource<T> {
export abstract class TbTableDatasource<T> implements DataSource<T> {
protected dataSubject = new BehaviorSubject<Array<T>>([]);

4
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts

@ -35,9 +35,9 @@ import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { TranslateService } from '@ngx-translate/core';
import { generateSecret } from '@core/utils';
import { SecurityConfigComponent } from '@home/components/widget/lib/gateway/connectors-configuration/public-api';
import { Subject } from 'rxjs';
import { GatewayPortTooltipPipe } from '@home/pipes/public-api';
import { GatewayPortTooltipPipe } from '@home/pipes/gateway-port-tooltip.pipe';
import { SecurityConfigComponent } from '../security-config/security-config.component';
@Component({
selector: 'tb-broker-config-control',

4
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts

@ -58,7 +58,7 @@ import { coerceBoolean } from '@shared/decorators/coercion';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { TruncateTooltipDirective } from '@shared/directives/truncate-tooltip.directive';
import { TbDatasource } from '@shared/abstract/public-api';
import { TbTableDatasource } from '@home/components/table/table-datasource.abstract';
@Component({
selector: 'tb-mapping-table',
@ -311,7 +311,7 @@ export class MappingTableComponent implements ControlValueAccessor, Validator, A
}
}
export class MappingDatasource extends TbDatasource<MappingValue> {
export class MappingDatasource extends TbTableDatasource<MappingValue> {
constructor() {
super();
}

16
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts

@ -30,7 +30,7 @@ import { CommonModule } from '@angular/common';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { EllipsisChipListDirective } from '@shared/directives/public-api';
import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list.directive';
import { ModbusSlaveConfigComponent } from '../modbus-slave-config/modbus-slave-config.component';
import { ModbusMasterTableComponent } from '../modbus-master-table/modbus-master-table.component';
@ -62,9 +62,10 @@ import { ModbusMasterTableComponent } from '../modbus-master-table/modbus-master
:host {
height: 100%;
}
:host ::ng-deep {
.mat-mdc-tab-group, .mat-mdc-tab-body-wrapper {
height: 100%;
.mat-mdc-tab-body-content {
overflow: hidden !important;
}
}
`]
@ -76,10 +77,9 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat
basicFormGroup: FormGroup;
onChange: (value: string) => void;
onChange: (value: ModbusBasicConfig) => void;
onTouched: () => void;
protected readonly connectorType = ConnectorType;
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder) {
@ -101,7 +101,7 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat
this.destroy$.complete();
}
registerOnChange(fn: (value: string) => void): void {
registerOnChange(fn: (value: ModbusBasicConfig) => void): void {
this.onChange = fn;
}
@ -111,8 +111,8 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat
writeValue(basicConfig: ModbusBasicConfig): void {
const editedBase = {
slave: basicConfig.slave || {},
master: basicConfig.master || {},
slave: basicConfig.slave ?? {},
master: basicConfig.master ?? {},
};
this.basicFormGroup.setValue(editedBase, {emitEvent: false});

23
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html

@ -20,7 +20,7 @@
<div class="tb-form-panel-title">{{ panelTitle | translate }}{{' (' + keysListFormArray.controls.length + ')'}}</div>
<div class="tb-form-panel no-border no-padding key-panel" *ngIf="keysListFormArray.controls.length; else noKeys">
<div class="tb-form-panel no-border no-padding tb-flex no-flex row center fill-width"
*ngFor="let keyControl of keysListFormArray.controls; trackBy: trackByKey; let $index = index; let last = last;">
*ngFor="let keyControl of keysListFormArray.controls; trackBy: trackByControlId; let $index = index; let last = last;">
<div class="tb-form-panel stroked tb-flex">
<ng-container [formGroup]="keyControl">
<mat-expansion-panel class="tb-settings" [expanded]="last">
@ -76,7 +76,12 @@
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="functionCode">
<mat-option *ngFor="let code of functionCodesMap.get(keyControl.get('id').value) || defaultFunctionCodes" [value]="code">{{ ModbusFunctionCodeTranslationsMap.get(code) | translate }}</mat-option>
<mat-option
*ngFor="let code of functionCodesMap.get(keyControl.get('id').value) || defaultFunctionCodes"
[value]="code"
>
{{ ModbusFunctionCodeTranslationsMap.get(code) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
@ -89,6 +94,7 @@
matInput
type="number"
min="1"
max="50000"
name="value"
formControlName="objectsCount"
placeholder="{{ 'gateway.set' | translate }}"
@ -98,10 +104,19 @@
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.address</div>
<div class="fixed-title-width tb-required" translate>gateway.address</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="address" placeholder="{{ 'gateway.set' | translate }}"/>
<input matInput type="number" min="0" max="50000" name="value" formControlName="address" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.address-required') | translate"
*ngIf="keyControl.get('address').hasError('required') &&
keyControl.get('address').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>

110
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts

@ -14,22 +14,32 @@
/// limitations under the License.
///
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { AbstractControl, FormGroup, UntypedFormArray, UntypedFormBuilder, Validators } from '@angular/forms';
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import {
AbstractControl,
FormArray,
FormGroup,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormGroup,
Validators
} from '@angular/forms';
import { TbPopoverComponent } from '@shared/components/popover.component';
import {
ModbusDataType,
ModbusFunctionCodeTranslationsMap,
ModbusObjectCountByDataType,
ModbusRegisterType,
ModbusValue,
ModbusValueKey,
noLeadTrailSpacesRegex,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { GatewayHelpLinkPipe } from '@home/pipes/public-api';
import { GatewayHelpLinkPipe } from '@home/pipes/gateway-help-link.pipe';
import { generateSecret } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
@Component({
selector: 'tb-modbus-data-keys-panel',
@ -42,22 +52,21 @@ import { generateSecret } from '@core/utils';
GatewayHelpLinkPipe,
]
})
export class ModbusDataKeysPanelComponent implements OnInit {
export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy {
@coerceBoolean()
@Input() isMaster = false;
@Input() panelTitle: string;
@Input() addKeyTitle: string;
@Input() deleteKeyTitle: string;
@Input() noKeysText: string;
@Input() register: ModbusRegisterType;
@Input() keysType: ModbusValueKey;
@Input() values: ModbusValue[];
@Input() popover: TbPopoverComponent<ModbusDataKeysPanelComponent>;
@Output() keysDataApplied = new EventEmitter<Array<ModbusValue>>();
keysListFormArray: UntypedFormArray;
errorText = '';
keysListFormArray: FormArray<UntypedFormGroup>;
modbusDataTypes = Object.values(ModbusDataType);
withFunctionCode = true;
functionCodesMap = new Map();
@ -65,20 +74,28 @@ export class ModbusDataKeysPanelComponent implements OnInit {
readonly editableDataTypes = [ModbusDataType.BYTES, ModbusDataType.BITS, ModbusDataType.STRING];
readonly ModbusFunctionCodeTranslationsMap = ModbusFunctionCodeTranslationsMap;
readonly defaultReadFunctionCodes = [3, 4];
readonly defaultWriteFunctionCodes = [5, 6, 15, 16];
readonly stringAttrUpdatesWriteFunctionCodes = [6, 16];
private destroy$ = new Subject<void>();
private readonly defaultReadFunctionCodes = [3, 4];
private readonly defaultWriteFunctionCodes = [5, 6, 15, 16];
private readonly stringAttrUpdatesWriteFunctionCodes = [6, 16];
constructor(private fb: UntypedFormBuilder) {}
ngOnInit(): void {
this.keysListFormArray = this.prepareKeysFormArray(this.values);
this.withFunctionCode = !this.isMaster || (this.keysType !== ModbusValueKey.ATTRIBUTES && this.keysType !== ModbusValueKey.TIMESERIES);
this.keysListFormArray = this.prepareKeysFormArray(this.values);
this.defaultFunctionCodes = this.getDefaultFunctionCodes();
}
trackByKey(_: number, keyControl: AbstractControl): AbstractControl {
return keyControl;
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
trackByControlId(_: number, keyControl: AbstractControl): string {
return keyControl.value.id;
}
addKey(): void {
@ -86,9 +103,9 @@ export class ModbusDataKeysPanelComponent implements OnInit {
tag: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
value: [{value: '', disabled: !this.isMaster}, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
type: [ModbusDataType.BYTES, [Validators.required]],
address: [0, [Validators.required]],
address: [null, [Validators.required]],
objectsCount: [1, [Validators.required]],
functionCode: [this.getDefaultFunctionCodes()[0]],
functionCode: [{ value: this.getDefaultFunctionCodes()[0], disabled: !this.withFunctionCode }, [Validators.required]],
id: [{value: generateSecret(5), disabled: true}],
});
this.observeKeyDataType(dataKeyFormGroup);
@ -105,7 +122,7 @@ export class ModbusDataKeysPanelComponent implements OnInit {
}
cancel(): void {
this.popover?.hide();
this.popover.hide();
}
applyKeysData(): void {
@ -114,53 +131,68 @@ export class ModbusDataKeysPanelComponent implements OnInit {
private prepareKeysFormArray(values: ModbusValue[]): UntypedFormArray {
const keysControlGroups: Array<AbstractControl> = [];
if (values) {
values.forEach(keyData => {
const { tag, value, type, address, objectsCount, functionCode } = keyData;
const dataKeyFormGroup = this.fb.group({
tag: [tag, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
value: [{value, disabled: !this.isMaster}, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
type: [type, [Validators.required]],
address: [address, [Validators.required]],
objectsCount: [objectsCount, [Validators.required]],
functionCode: [functionCode, []],
id: [{value: generateSecret(5), disabled: true}],
});
values.forEach(value => {
const dataKeyFormGroup = this.createDataKeyFormGroup(value);
this.observeKeyDataType(dataKeyFormGroup);
this.functionCodesMap.set(dataKeyFormGroup.get('id').value, this.getFunctionCodes(type));
this.functionCodesMap.set(dataKeyFormGroup.get('id').value, this.getFunctionCodes(value.type));
keysControlGroups.push(dataKeyFormGroup);
});
}
return this.fb.array(keysControlGroups);
}
private createDataKeyFormGroup(modbusValue: ModbusValue): FormGroup {
const { tag, value, type, address, objectsCount, functionCode } = modbusValue;
return this.fb.group({
tag: [tag, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
value: [{ value, disabled: !this.isMaster }, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
type: [type, [Validators.required]],
address: [address, [Validators.required]],
objectsCount: [objectsCount, [Validators.required]],
functionCode: [{ value: functionCode, disabled: !this.withFunctionCode }, [Validators.required]],
id: [{ value: generateSecret(5), disabled: true }],
});
}
private observeKeyDataType(keyFormGroup: FormGroup): void {
keyFormGroup.get('type').valueChanges.subscribe(dataType => {
const objectsCountControl = keyFormGroup.get('objectsCount');
keyFormGroup.get('type').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(dataType => {
if (!this.editableDataTypes.includes(dataType)) {
objectsCountControl.patchValue(ModbusObjectCountByDataType[dataType]);
keyFormGroup.get('objectsCount').patchValue(ModbusObjectCountByDataType[dataType], {emitEvent: false});
}
this.functionCodesMap.set(keyFormGroup.get('id').value, this.getFunctionCodes(dataType));
this.updateFunctionCodes(keyFormGroup, dataType);
});
}
private updateFunctionCodes(keyFormGroup: FormGroup, dataType: ModbusDataType): void {
const functionCodes = this.getFunctionCodes(dataType);
this.functionCodesMap.set(keyFormGroup.get('id').value, functionCodes);
if (!functionCodes.includes(keyFormGroup.get('functionCode').value)) {
keyFormGroup.get('functionCode').patchValue(functionCodes[0], {emitEvent: false});
}
}
private getFunctionCodes(dataType: ModbusDataType): number[] {
if (this.keysType === ModbusValueKey.ATTRIBUTES_UPDATES) {
if (dataType === ModbusDataType.STRING) {
return this.stringAttrUpdatesWriteFunctionCodes;
}
return this.defaultWriteFunctionCodes;
return dataType === ModbusDataType.STRING
? this.stringAttrUpdatesWriteFunctionCodes
: this.defaultWriteFunctionCodes;
}
const functionCodes = [...this.defaultReadFunctionCodes];
if (dataType === ModbusDataType.BITS) {
const bitsFunctionCodes = [1, 2];
bitsFunctionCodes.forEach(code => functionCodes.push(code));
functionCodes.push(...bitsFunctionCodes);
functionCodes.sort();
}
if (this.keysType === ModbusValueKey.RPC_REQUESTS) {
this.defaultWriteFunctionCodes.forEach(code => functionCodes.push(code));
functionCodes.push(...this.defaultWriteFunctionCodes);
}
return functionCodes;
}

12
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html

@ -60,19 +60,19 @@
<div class="table-container">
<table mat-table [dataSource]="dataSource">
<ng-container [matColumnDef]="'name'">
<mat-header-cell *matHeaderCellDef class="table-value-column request-column">
<mat-header-cell *matHeaderCellDef class="table-value-column">
{{ 'gateway.name' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let mapping" class="table-value-column request-column">
<mat-cell *matCellDef="let mapping" class="table-value-column">
{{ mapping['name'] }}
</mat-cell>
</ng-container>
<ng-container [matColumnDef]="'type'">
<mat-header-cell *matHeaderCellDef class="table-value-column request-column">
<mat-header-cell *matHeaderCellDef class="table-value-column">
{{ 'gateway.client-communication-type' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let mapping" class="table-value-column request-column">
{{ ModbusClientTypeLabelsMap.get(mapping['type']) }}
<mat-cell *matCellDef="let mapping" class="table-value-column">
{{ ModbusProtocolLabelsMap.get(mapping['type']) }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
@ -111,7 +111,7 @@
</mat-cell>
</ng-container>
<mat-header-row [ngClass]="{'mat-row-select': true}" *matHeaderRowDef="['name', 'type', 'actions']; sticky: true"></mat-header-row>
<mat-row *matRowDef="let mapping; columns: ['name', 'type', 'actions'];"></mat-row>
<mat-row *matRowDef="let mapping; columns: ['name', 'type', 'actions']"></mat-row>
</table>
<section [fxShow]="!textSearchMode && (dataSource.isEmpty() | async)" fxLayoutAlign="center center"
class="mat-headline-5 tb-absolute-fill tb-add-new">

21
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.scss

@ -19,19 +19,15 @@
width: 100%;
height: 100%;
display: block;
.tb-master-table {
.tb-master-table-content {
width: 100%;
height: 100%;
background: #fff;
overflow: hidden;
&.tb-outlined-border {
box-shadow: 0 0 0 0 rgb(0 0 0 / 20%), 0 0 0 0 rgb(0 0 0 / 14%), 0 0 0 0 rgb(0 0 0 / 12%);
border: solid 1px #e0e0e0;
border-radius: 4px;
}
.mat-toolbar-tools{
min-height: auto;
}
@ -49,25 +45,17 @@
.table-container {
overflow: auto;
.mat-mdc-table {
table-layout: fixed;
min-width: 450px;
.table-value-column {
padding: 0 12px;
width: 23%;
&.request-column {
width: 38%;
}
width: 38%;
}
}
}
.ellipsis {
overflow: hidden;
text-overflow: ellipsis;
}
}
}
@ -91,6 +79,7 @@
:host ::ng-deep {
mat-cell.tb-value-cell {
cursor: pointer;
.mat-icon {
height: 24px;
width: 24px;

55
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts

@ -17,6 +17,7 @@
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
forwardRef,
@ -40,13 +41,15 @@ import {
Validator,
} from '@angular/forms';
import {
ModbusClientTypeLabelsMap, ModbusMasterConfig, SlaveConfig,
ModbusMasterConfig,
ModbusProtocolLabelsMap,
SlaveConfig
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { TbDatasource } from '@shared/abstract/datasource/datasource.abstract';
import { ModbusSlaveDialogComponent } from '../modbus-slave-dialog/modbus-slave-dialog.component';
import { TbTableDatasource } from '@home/components/table/table-datasource.abstract';
@Component({
selector: 'tb-modbus-master-table',
@ -74,15 +77,12 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat
textSearchMode = false;
dataSource: SlavesDatasource;
hidePageSize = false;
activeValue = false;
dirtyValue = false;
masterFormGroup: UntypedFormGroup;
textSearch = this.fb.control('', {nonNullable: true});
readonly ModbusClientTypeLabelsMap = ModbusClientTypeLabelsMap;
readonly ModbusProtocolLabelsMap = ModbusProtocolLabelsMap;
private onChange: (value: string) => void = () => {};
private onChange: (value: ModbusMasterConfig) => void = () => {};
private onTouched: () => void = () => {};
private destroy$ = new Subject<void>();
@ -91,10 +91,10 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat
public translate: TranslateService,
public dialog: MatDialog,
private dialogService: DialogService,
private fb: FormBuilder
private fb: FormBuilder,
private cdr: ChangeDetectorRef,
) {
this.masterFormGroup = this.fb.group({ slaves: this.fb.array([])});
this.dirtyValue = !this.activeValue;
this.masterFormGroup = this.fb.group({ slaves: this.fb.array([]) });
this.dataSource = new SlavesDatasource();
}
@ -122,13 +122,10 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat
debounceTime(150),
distinctUntilChanged((prev, current) => (prev ?? '') === current.trim()),
takeUntil(this.destroy$)
).subscribe((text) => {
const searchText = text.trim();
this.updateTableData(this.slaves.value, searchText.trim());
});
).subscribe(text => this.updateTableData(this.slaves.value, text.trim()));
}
registerOnChange(fn: (value: string) => void): void {
registerOnChange(fn: (value: ModbusMasterConfig) => void): void {
this.onChange = fn;
}
@ -149,10 +146,10 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat
enterFilterMode(): void {
this.textSearchMode = true;
setTimeout(() => {
this.searchInputField.nativeElement.focus();
this.searchInputField.nativeElement.setSelectionRange(0, 0);
}, 10);
this.cdr.detectChanges();
const searchInput = this.searchInputField.nativeElement;
searchInput.focus();
searchInput.setSelectionRange(0, 0);
}
exitFilterMode(): void {
@ -165,19 +162,20 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat
if ($event) {
$event.stopPropagation();
}
const value = isDefinedAndNotNull(index) ? this.slaves.at(index).value : {};
const withIndex = isDefinedAndNotNull(index);
const value = withIndex ? this.slaves.at(index).value : {};
this.dialog.open<ModbusSlaveDialogComponent, any, any>(ModbusSlaveDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
value,
buttonTitle: isUndefinedOrNull(index) ? 'action.add' : 'action.apply'
buttonTitle: withIndex ? 'action.add' : 'action.apply'
}
}).afterClosed()
.pipe(take(1), takeUntil(this.destroy$))
.subscribe(res => {
if (res) {
if (isDefinedAndNotNull(index)) {
if (withIndex) {
this.slaves.at(index).patchValue(res);
} else {
this.slaves.push(this.fb.control(res));
@ -197,7 +195,7 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat
this.translate.instant('action.no'),
this.translate.instant('action.yes'),
true
).subscribe((result) => {
).pipe(take(1), takeUntil(this.destroy$)).subscribe((result) => {
if (result) {
this.slaves.removeAt(index);
this.masterFormGroup.markAsDirty();
@ -206,15 +204,14 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat
}
private updateTableData(data: SlaveConfig[], textSearch?: string): void {
let tableValue = data;
if (textSearch) {
tableValue = tableValue.filter(value =>
Object.values(value).some(val =>
val.toString().toLowerCase().includes(textSearch.toLowerCase())
data = data.filter(item =>
Object.values(item).some(value =>
value.toString().toLowerCase().includes(textSearch.toLowerCase())
)
);
}
this.dataSource.loadData(tableValue);
this.dataSource.loadData(data);
}
private pushDataAsFormArrays(slaves: SlaveConfig[]): void {
@ -224,7 +221,7 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat
}
}
export class SlavesDatasource extends TbDatasource<SlaveConfig> {
export class SlavesDatasource extends TbTableDatasource<SlaveConfig> {
constructor() {
super();
}

68
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts

@ -14,7 +14,15 @@
/// limitations under the License.
///
import { ChangeDetectionStrategy, Component, forwardRef, Input, OnChanges, OnDestroy } from '@angular/core';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
forwardRef,
Input,
OnChanges,
OnDestroy
} from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
@ -31,9 +39,9 @@ import {
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { SecurityConfigComponent } from '@home/components/widget/lib/gateway/connectors-configuration/public-api';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-modbus-security-config',
@ -55,47 +63,36 @@ import { takeUntil } from 'rxjs/operators';
imports: [
CommonModule,
SharedModule,
SecurityConfigComponent,
]
})
export class ModbusSecurityConfigComponent implements ControlValueAccessor, Validator, OnChanges, OnDestroy {
@coerceBoolean()
@Input() isMaster = false;
@Input() disabled = false;
securityConfigFormGroup: UntypedFormGroup;
private disabled = false;
private onChange: (value: ModbusSecurity) => void;
private onTouched: () => void;
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder) {
constructor(private fb: FormBuilder, private cdr: ChangeDetectorRef) {
this.securityConfigFormGroup = this.fb.group({
certfile: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
keyfile: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
password: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
server_hostname: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
reqclicert: [{value: false, disabled: true}],
});
this.observeValueChanges();
}
ngOnChanges(): void {
if (this.isMaster) {
this.securityConfigFormGroup = this.fb.group({
certfile: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
keyfile: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
password: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
reqclicert: [false, []],
});
this.observeValueChanges();
}
if (this.disabled) {
this.securityConfigFormGroup.disable({emitEvent:false});
} else {
this.securityConfigFormGroup.enable({emitEvent:false});
}
this.updateMasterEnabling();
}
ngOnDestroy(): void {
@ -111,27 +108,48 @@ export class ModbusSecurityConfigComponent implements ControlValueAccessor, Vali
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.securityConfigFormGroup.disable({emitEvent: false});
} else {
this.securityConfigFormGroup.enable({emitEvent: false});
}
this.updateMasterEnabling();
this.cdr.markForCheck();
}
validate(): ValidationErrors | null {
return this.securityConfigFormGroup.valid || this.disabled ? null : {
return this.securityConfigFormGroup.valid ? null : {
securityConfigFormGroup: { valid: false }
};
}
writeValue(securityConfig: ModbusSecurity): void {
const { certfile, password, keyfile, server_hostname } = securityConfig;
let securityState = {
const securityState = {
certfile: certfile ?? '',
password: password ?? '',
keyfile: keyfile ?? '',
server_hostname: server_hostname?? '',
server_hostname: server_hostname ?? '',
reqclicert: !!securityConfig.reqclicert,
};
this.securityConfigFormGroup.reset(securityState, {emitEvent: false});
}
private updateMasterEnabling(): void {
if (this.isMaster) {
securityState = { ...securityState, reqclicert: !!securityConfig.reqclicert };
if (!this.disabled) {
this.securityConfigFormGroup.get('reqclicert').enable({emitEvent: false});
}
this.securityConfigFormGroup.get('server_hostname').disable({emitEvent: false});
} else {
securityState = { ...securityState, server_hostname: server_hostname ?? '' };
if (!this.disabled) {
this.securityConfigFormGroup.get('server_hostname').enable({emitEvent: false});
}
this.securityConfigFormGroup.get('reqclicert').disable({emitEvent: false});
}
this.securityConfigFormGroup.reset(securityState, {emitEvent: false});
}
private observeValueChanges(): void {

403
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html

@ -15,242 +15,249 @@
limitations under the License.
-->
<div class="tb-form-panel no-border no-padding padding-top" [formGroup]="slaveConfigFormGroup">
<div class="tb-form-hint tb-primary-fill tb-flex center">{{ 'gateway.hints.modbus-server' | translate }}</div>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="sendDataToThingsBoard">
<mat-label>
{{ 'gateway.enable' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
<div class="tb-flex row space-between align-center no-gap fill-width">
<div class="fixed-title-width" translate>gateway.server-slave-config</div>
<tb-toggle-select formControlName="type" appearance="fill">
<tb-toggle-option *ngFor="let type of modbusProtocolTypes" [value]="type">{{ ModbusProtocolLabelsMap.get(type) }}</tb-toggle-option>
</tb-toggle-select>
</div>
<div [formGroup]="slaveConfigFormGroup" class="slave-container">
<div class="tb-form-panel no-border no-padding padding-top">
<div *ngIf="this.slaveConfigFormGroup.get('type').value !== ModbusProtocolType.Serial"
class="tb-form-row column-xs"
fxLayoutAlign="space-between center"
>
<div class="fixed-title-width tb-required" translate>gateway.host</div>
<div class="tb-form-hint tb-primary-fill tb-flex center">{{ 'gateway.hints.modbus-server' | translate }}</div>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="sendDataToThingsBoard">
<mat-label>
{{ 'gateway.enable' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
</div>
<div class="slave-content tb-form-panel no-border no-padding padding-top" >
<div class="tb-flex row space-between align-center no-gap fill-width">
<div class="fixed-title-width" translate>gateway.server-slave-config</div>
<tb-toggle-select formControlName="type" appearance="fill">
<tb-toggle-option *ngFor="let type of modbusProtocolTypes" [value]="type">{{ ModbusProtocolLabelsMap.get(type) }}</tb-toggle-option>
</tb-toggle-select>
</div>
<div class="tb-form-panel no-border no-padding padding-top">
<div *ngIf="protocolType !== ModbusProtocolType.Serial"
class="tb-form-row column-xs"
fxLayoutAlign="space-between center"
>
<div class="fixed-title-width tb-required" translate>gateway.host</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="host" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.host-required') | translate"
*ngIf="slaveConfigFormGroup.get('host').hasError('required')
&& slaveConfigFormGroup.get('host').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div *ngIf="protocolType !== ModbusProtocolType.Serial else serialPort"
class="tb-form-row column-xs"
fxLayoutAlign="space-between center"
>
<div class="fixed-title-width tb-required" translate>gateway.port</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="{{portLimits.MIN}}" max="{{portLimits.MAX}}"
name="value" formControlName="port" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="slaveConfigFormGroup.get('port') | getGatewayPortTooltip"
*ngIf="(slaveConfigFormGroup.get('port').hasError('required') ||
slaveConfigFormGroup.get('port').hasError('min') ||
slaveConfigFormGroup.get('port').hasError('max')) &&
slaveConfigFormGroup.get('port').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<ng-template #serialPort>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.port</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="serialPort" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'gateway.port-required' | translate"
*ngIf="slaveConfigFormGroup.get('port').hasError('required') && slaveConfigFormGroup.get('port').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
</ng-template>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.framer-type' | translate }}" translate>
gateway.method
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="method">
<mat-option *ngFor="let method of protocolType === ModbusProtocolType.Serial ? modbusSerialMethodTypes : modbusMethodTypes"
[value]="method">{{ ModbusMethodLabelsMap.get(method) }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.unit-id</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="host" placeholder="{{ 'gateway.set' | translate }}"/>
<input matInput type="number" min="0" name="value" formControlName="unitId" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.host-required') | translate"
*ngIf="slaveConfigFormGroup.get('host').hasError('required')
&& slaveConfigFormGroup.get('host').touched"
[matTooltip]="('gateway.unit-id-required') | translate"
*ngIf="slaveConfigFormGroup.get('unitId').hasError('required') &&
slaveConfigFormGroup.get('unitId').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div *ngIf="this.slaveConfigFormGroup.get('type').value !== ModbusProtocolType.Serial else serialPort"
class="tb-form-row column-xs"
fxLayoutAlign="space-between center"
>
<div class="fixed-title-width tb-required" translate>gateway.port</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.device-name</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="{{portLimits.MIN}}" max="{{portLimits.MAX}}"
name="value" formControlName="port" placeholder="{{ 'gateway.set' | translate }}"/>
<input matInput name="value" formControlName="deviceName" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="slaveConfigFormGroup.get('port') | getGatewayPortTooltip"
*ngIf="(slaveConfigFormGroup.get('port').hasError('required') ||
slaveConfigFormGroup.get('port').hasError('min') ||
slaveConfigFormGroup.get('port').hasError('max')) &&
slaveConfigFormGroup.get('port').touched"
[matTooltip]="('gateway.device-name-required') | translate"
*ngIf="slaveConfigFormGroup.get('deviceName').hasError('required') &&
slaveConfigFormGroup.get('deviceName').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<ng-template #serialPort>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.port</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="serialPort" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="slaveConfigFormGroup.get('serialPort') | getGatewayPortTooltip"
*ngIf="(slaveConfigFormGroup.get('serialPort').hasError('required') ||
slaveConfigFormGroup.get('serialPort').hasError('min') ||
slaveConfigFormGroup.get('serialPort').hasError('max')) &&
slaveConfigFormGroup.get('serialPort').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.device-profile</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="deviceType" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.device-profile-required') | translate"
*ngIf="slaveConfigFormGroup.get('deviceType').hasError('required') &&
slaveConfigFormGroup.get('deviceType').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</ng-template>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.framer-type' | translate }}" translate>
gateway.method
<div class="fixed-title-width" translate>gateway.poll-period</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="pollPeriod" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div *ngIf="protocolType === ModbusProtocolType.Serial" class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.baudrate</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="method">
<mat-option *ngFor="let method of modbusMethodTypes" [value]="method">{{ ModbusMethodLabelsMap.get(method) }}</mat-option>
<mat-select formControlName="baudrate">
<mat-option *ngFor="let rate of modbusBaudrates" [value]="rate">{{ rate }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.unit-id</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="unitId" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.unit-id-required') | translate"
*ngIf="slaveConfigFormGroup.get('unitId').hasError('required') &&
slaveConfigFormGroup.get('unitId').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.device-name</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="deviceName" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.device-name-required') | translate"
*ngIf="slaveConfigFormGroup.get('deviceName').hasError('required') &&
slaveConfigFormGroup.get('deviceName').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.device-profile</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="deviceType" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.device-profile-required') | translate"
*ngIf="slaveConfigFormGroup.get('deviceType').hasError('required') &&
slaveConfigFormGroup.get('deviceType').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.poll-period</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="pollPeriod" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div *ngIf="this.slaveConfigFormGroup.get('type').value === ModbusProtocolType.Serial" class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.baudrate</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="baudrate" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
<div class="tb-form-panel-title" translate>gateway.advanced-connection-settings</div>
</mat-panel-title>
</mat-expansion-panel-header>
<div class="tb-form-panel no-border no-padding padding-top" [formGroup]="slaveConfigFormGroup">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.byte-order</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="byteOrder">
<mat-option *ngFor="let order of modbusOrderType" [value]="order">{{ order }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<mat-expansion-panel *ngIf="this.slaveConfigFormGroup.get('type').value !== ModbusProtocolType.Serial"
[expanded]="showSecurityControl.value">
<mat-expansion-panel-header class="nested-expansion-header">
<div class="tb-form-panel stroked">
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header>
<mat-panel-title>
<mat-slide-toggle [formControl]="showSecurityControl" class="mat-slide" (click)="$event.stopPropagation()">
<mat-label>
{{ 'gateway.tls-connection' | translate }}
</mat-label>
</mat-slide-toggle>
<div class="tb-form-panel-title" translate>gateway.advanced-connection-settings</div>
</mat-panel-title>
</mat-expansion-panel-header>
<tb-modbus-security-config formControlName="security" [disabled]="!slaveConfigFormGroup.get('sendDataToThingsBoard').value || !showSecurityControl.value" [isMaster]="true"></tb-modbus-security-config>
</mat-expansion-panel>
<ng-container [formGroup]="slaveConfigFormGroup.get('identity')">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.vendor-name</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="vendorName" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.product-code</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="productCode" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.vendor-url</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="vendorUrl" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
<div class="tb-form-panel no-border no-padding padding-top">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.byte-order</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="byteOrder">
<mat-option *ngFor="let order of modbusOrderType" [value]="order">{{ order }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.product-name</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="productName" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
<div *ngIf="protocolType !== ModbusProtocolType.Serial" class="tb-form-panel stroked tb-slide-toggle">
<mat-expansion-panel class="tb-settings" [expanded]="showSecurityControl.value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle fxLayoutAlign="center" [formControl]="showSecurityControl" class="mat-slide" (click)="$event.stopPropagation()">
<mat-label>
{{ 'gateway.tls-connection' | translate }}
</mat-label>
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<tb-modbus-security-config formControlName="security"></tb-modbus-security-config>
</mat-expansion-panel>
</div>
<ng-container [formGroup]="slaveConfigFormGroup.get('identity')">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.vendor-name</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="vendorName" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.product-code</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="productCode" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.vendor-url</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="vendorUrl" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.product-name</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="productName" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.model-name</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="modelName" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
</ng-container>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.model-name</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="modelName" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
</ng-container>
</mat-expansion-panel>
</div>
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>gateway.values</div>
<tb-modbus-values formControlName="values"></tb-modbus-values>
</div>
</mat-expansion-panel>
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>gateway.values</div>
<tb-modbus-values formControlName="values" [disabled]="!slaveConfigFormGroup.get('sendDataToThingsBoard').value"></tb-modbus-values>
</div>
</div>

28
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.scss

@ -0,0 +1,28 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
$server-config-header-height: 132px;
:host {
.slave-content {
height: calc(100% - #{$server-config-header-height});
overflow: auto;
}
.slave-container {
display: inherit;
}
}

199
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts

@ -18,21 +18,23 @@ import { ChangeDetectionStrategy, Component, forwardRef, OnDestroy } from '@angu
import {
ControlValueAccessor,
FormBuilder,
FormControl,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormControl,
UntypedFormGroup,
ValidationErrors,
Validator,
Validators,
} from '@angular/forms';
import {
ModbusBaudrates,
ModbusMethodLabelsMap,
ModbusMethodType,
ModbusOrderType,
ModbusProtocolLabelsMap,
ModbusProtocolType,
ModbusRegisterValues,
ModbusSerialMethodType,
ModbusSlave,
noLeadTrailSpacesRegex,
PortLimits,
@ -42,9 +44,10 @@ import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { Subject } from 'rxjs';
import { startWith, takeUntil } from 'rxjs/operators';
import { GatewayPortTooltipPipe } from '@home/pipes/public-api';
import { GatewayPortTooltipPipe } from '@home/pipes/gateway-port-tooltip.pipe';
import { ModbusSecurityConfigComponent } from '../modbus-security-config/modbus-security-config.component';
import { ModbusValuesComponent, } from '../modbus-values/modbus-values.component';
import { isEqual } from '@core/utils';
@Component({
selector: 'tb-modbus-slave-config',
@ -70,30 +73,25 @@ import { ModbusValuesComponent, } from '../modbus-values/modbus-values.component
ModbusSecurityConfigComponent,
GatewayPortTooltipPipe,
],
styles: [`
:host {
.nested-expansion-header {
.mat-content {
height: 100%;
}
}
}
`],
styleUrls: ['modbus-slave-config.component.scss'],
})
export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validator, OnDestroy {
slaveConfigFormGroup: UntypedFormGroup;
showSecurityControl: UntypedFormControl;
showSecurityControl: FormControl<boolean>;
ModbusProtocolLabelsMap = ModbusProtocolLabelsMap;
ModbusMethodLabelsMap = ModbusMethodLabelsMap;
portLimits = PortLimits;
readonly modbusProtocolTypes = Object.values(ModbusProtocolType);
readonly modbusMethodTypes = Object.values(ModbusMethodType);
readonly modbusSerialMethodTypes = Object.values(ModbusSerialMethodType);
readonly modbusOrderType = Object.values(ModbusOrderType);
readonly ModbusProtocolType = ModbusProtocolType;
readonly serialSpecificControlKeys = ['serialPort', 'baudrate'];
readonly tcpUdpSpecificControlKeys = ['port', 'security', 'host'];
readonly modbusBaudrates = ModbusBaudrates;
private readonly serialSpecificControlKeys = ['serialPort', 'baudrate'];
private readonly tcpUdpSpecificControlKeys = ['port', 'security', 'host'];
private onChange: (value: SlaveConfig) => void;
private onTouched: () => void;
@ -103,18 +101,18 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat
constructor(private fb: FormBuilder) {
this.showSecurityControl = this.fb.control(false);
this.slaveConfigFormGroup = this.fb.group({
type: [ModbusProtocolType.TCP, []],
type: [ModbusProtocolType.TCP],
host: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]],
serialPort: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
method: [ModbusMethodType.SOCKET, []],
unitId: [null, [Validators.required]],
baudrate: [null, []],
method: [ModbusMethodType.SOCKET],
unitId: [0, [Validators.required]],
baudrate: [this.modbusBaudrates[0]],
deviceName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
deviceType: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
pollPeriod: [null, []],
sendDataToThingsBoard: [false, []],
byteOrder:[ModbusOrderType.BIG, []],
pollPeriod: [5000],
sendDataToThingsBoard: [false],
byteOrder:[ModbusOrderType.BIG],
security: [],
identity: this.fb.group({
vendorName: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
@ -126,19 +124,18 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat
values: [],
});
this.slaveConfigFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value: SlaveConfig) => {
if (value.type === ModbusProtocolType.Serial) {
value.port = value.serialPort;
delete value.serialPort;
}
this.onChange(value);
this.onTouched();
});
this.observeValueChanges();
this.observeTypeChange();
this.observeFormEnable();
this.observeShowSecurity();
}
get isSlaveEnabled(): boolean {
return this.slaveConfigFormGroup.get('sendDataToThingsBoard').value;
}
get protocolType(): ModbusProtocolType {
return this.slaveConfigFormGroup.get('type').value;
}
ngOnDestroy(): void {
@ -156,28 +153,53 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat
validate(): ValidationErrors | null {
return this.slaveConfigFormGroup.valid ? null : {
serverConfigFormGroup: { valid: false }
slaveConfigFormGroup: { valid: false }
};
}
writeValue(slaveConfig: ModbusSlave): void {
this.showSecurityControl.patchValue(!!slaveConfig.security);
this.showSecurityControl.patchValue(!!slaveConfig.security && !isEqual(slaveConfig.security, {}));
this.updateSlaveConfig(slaveConfig);
this.updateFormEnableState(slaveConfig.sendDataToThingsBoard);
}
private observeTypeChange(): void {
this.slaveConfigFormGroup.get('type').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(type => {
this.updateFormEnableState(this.slaveConfigFormGroup.get('sendDataToThingsBoard').value);
private observeValueChanges(): void {
this.slaveConfigFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value: SlaveConfig) => {
if (value.type === ModbusProtocolType.Serial) {
value.port = value.serialPort;
delete value.serialPort;
}
this.onChange(value);
this.onTouched();
});
}
private observeTypeChange(): void {
this.slaveConfigFormGroup.get('type').valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(type => {
this.updateFormEnableState(this.isSlaveEnabled);
this.updateMethodType(type);
});
}
private updateMethodType(type: ModbusProtocolType): void {
if (this.slaveConfigFormGroup.get('method').value !== ModbusMethodType.RTU) {
this.slaveConfigFormGroup.get('method').patchValue(
type === ModbusProtocolType.Serial
? ModbusSerialMethodType.ASCII
: ModbusMethodType.SOCKET,
{emitEvent: false}
);
}
}
private observeFormEnable(): void {
this.slaveConfigFormGroup.get('sendDataToThingsBoard').valueChanges
.pipe(startWith(this.slaveConfigFormGroup.get('sendDataToThingsBoard').value), takeUntil(this.destroy$))
.subscribe(value => {
this.updateFormEnableState(value);
});
.pipe(startWith(this.isSlaveEnabled), takeUntil(this.destroy$))
.subscribe(value => this.updateFormEnableState(value));
}
private updateFormEnableState(enabled: boolean): void {
@ -189,66 +211,77 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat
this.showSecurityControl.disable({emitEvent: false});
this.slaveConfigFormGroup.get('sendDataToThingsBoard').enable({emitEvent: false});
}
this.updateEnablingByProtocol(this.slaveConfigFormGroup.get('type').value);
this.updateEnablingByProtocol(this.protocolType);
this.updateSecurityEnable(this.showSecurityControl.value);
}
private updateEnablingByProtocol(type: ModbusProtocolType): void {
if (type === ModbusProtocolType.Serial) {
if (this.slaveConfigFormGroup.get('sendDataToThingsBoard').value) {
this.serialSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({emitEvent: false}));
}
this.tcpUdpSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.disable({emitEvent: false}));
private observeShowSecurity(): void {
this.showSecurityControl.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(value => this.updateSecurityEnable(value));
}
private updateSecurityEnable(securityEnabled: boolean): void {
if (securityEnabled && this.isSlaveEnabled && this.protocolType !== ModbusProtocolType.Serial) {
this.slaveConfigFormGroup.get('security').enable({emitEvent: false});
} else {
this.serialSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.disable({emitEvent: false}));
if (this.slaveConfigFormGroup.get('sendDataToThingsBoard').value) {
this.tcpUdpSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({emitEvent: false}));
}
this.slaveConfigFormGroup.get('security').disable({emitEvent: false});
}
};
}
private updateEnablingByProtocol(type: ModbusProtocolType): void {
const enableKeys = type === ModbusProtocolType.Serial ? this.serialSpecificControlKeys : this.tcpUdpSpecificControlKeys;
const disableKeys = type === ModbusProtocolType.Serial ? this.tcpUdpSpecificControlKeys : this.serialSpecificControlKeys;
if (this.isSlaveEnabled) {
enableKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({ emitEvent: false }));
}
disableKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.disable({ emitEvent: false }));
}
private updateSlaveConfig(slaveConfig: ModbusSlave): void {
const {
type = ModbusProtocolType.TCP,
method = ModbusMethodType.RTU,
unitId = 0,
deviceName = '',
deviceType = '',
pollPeriod = 5000,
sendDataToThingsBoard = false,
byteOrder = ModbusOrderType.BIG,
security = {},
identity = {
vendorName: '',
productCode: '',
vendorUrl: '',
productName: '',
modelName: '',
},
values = {} as ModbusRegisterValues,
baudrate = this.modbusBaudrates[0],
host = '',
port = null,
} = slaveConfig;
const slaveState: ModbusSlave = {
type,
method,
unitId,
deviceName,
deviceType,
pollPeriod,
sendDataToThingsBoard,
sendDataToThingsBoard: !!sendDataToThingsBoard,
byteOrder,
security,
identity,
values,
baudrate,
host,
port,
} = slaveConfig;
let slaveState: ModbusSlave = {
host: host ?? '',
type: type ?? ModbusProtocolType.TCP,
method: method ?? ModbusMethodType.SOCKET,
unitId: unitId ?? null,
deviceName: deviceName ?? '',
deviceType: deviceType ?? '',
pollPeriod: pollPeriod ?? null,
sendDataToThingsBoard: !!sendDataToThingsBoard,
byteOrder: byteOrder ?? ModbusOrderType.BIG,
security: security ?? {},
identity: identity ?? {
vendorName: '',
productCode: '',
vendorUrl: '',
productName: '',
modelName: '',
},
values: values ?? {} as ModbusRegisterValues,
port: port ?? null,
host: type === ModbusProtocolType.Serial ? '' : host,
port: type === ModbusProtocolType.Serial ? null : port,
serialPort: (type === ModbusProtocolType.Serial ? port : '') as string,
};
if (slaveConfig.type === ModbusProtocolType.Serial) {
slaveState = { ...slaveState, baudrate, serialPort: port, host: '', port: null } as ModbusSlave;
} else {
slaveState = { ...slaveState, serialPort: '', baudrate: null } as ModbusSlave;
}
this.slaveConfigFormGroup.setValue(slaveState, {emitEvent: false});
this.slaveConfigFormGroup.setValue(slaveState, { emitEvent: false });
}
}

222
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html

@ -53,7 +53,7 @@
</tb-toggle-select>
</div>
<div class="tb-form-panel no-border no-padding padding-top">
<div *ngIf="this.slaveConfigFormGroup.get('type').value !== ModbusProtocolType.Serial"
<div *ngIf="protocolType !== ModbusProtocolType.Serial"
class="tb-form-row column-xs"
fxLayoutAlign="space-between center"
>
@ -73,7 +73,7 @@
</mat-form-field>
</div>
</div>
<div *ngIf="this.slaveConfigFormGroup.get('type').value !== ModbusProtocolType.Serial else serialPort"
<div *ngIf="protocolType !== ModbusProtocolType.Serial else serialPort"
class="tb-form-row column-xs"
fxLayoutAlign="space-between center"
>
@ -105,10 +105,8 @@
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="slaveConfigFormGroup.get('serialPort') | getGatewayPortTooltip"
*ngIf="(slaveConfigFormGroup.get('serialPort').hasError('required') ||
slaveConfigFormGroup.get('serialPort').hasError('min') ||
slaveConfigFormGroup.get('serialPort').hasError('max')) &&
[matTooltip]="'gateway.port-required' | translate"
*ngIf="slaveConfigFormGroup.get('serialPort').hasError('required') &&
slaveConfigFormGroup.get('serialPort').touched"
class="tb-error">
warning
@ -124,18 +122,21 @@
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="method">
<mat-option *ngFor="let method of modbusMethodTypes" [value]="method">{{ ModbusMethodLabelsMap.get(method) }}</mat-option>
<mat-option *ngFor="let method of protocolType === ModbusProtocolType.Serial ? modbusSerialMethodTypes : modbusMethodTypes"
[value]="method">{{ ModbusMethodLabelsMap.get(method) }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</div>
<ng-container *ngIf="this.slaveConfigFormGroup.get('type').value === ModbusProtocolType.Serial">
<ng-container *ngIf="protocolType === ModbusProtocolType.Serial">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.baudrate</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="baudrate" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-select formControlName="baudrate">
<mat-option *ngFor="let rate of modbusBaudrates" [value]="rate">{{ rate }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
@ -143,7 +144,9 @@
<div class="fixed-title-width" translate>gateway.bytesize</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="bytesize" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-select formControlName="bytesize">
<mat-option *ngFor="let size of modbusByteSizes" [value]="size">{{ size }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
@ -159,7 +162,9 @@
<div class="fixed-title-width" translate>gateway.parity</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="parity" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-select formControlName="parity">
<mat-option *ngFor="let parity of modbusParities" [value]="parity">{{ ModbusParityLabelsMap.get(parity) }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
@ -229,110 +234,113 @@
</mat-label>
</mat-slide-toggle>
</div>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
<div class="tb-form-panel-title" translate>gateway.advanced-connection-settings</div>
</mat-panel-title>
</mat-expansion-panel-header>
<div class="tb-form-panel no-border no-padding padding-top">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.connection-timeout</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="timeout" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
<div class="tb-form-panel stroked">
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header>
<mat-panel-title>
<div class="tb-form-panel-title" translate>gateway.advanced-connection-settings</div>
</mat-panel-title>
</mat-expansion-panel-header>
<div class="tb-form-panel no-border no-padding padding-top">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.connection-timeout</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="timeout" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.byte-order</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="byteOrder">
<mat-option *ngFor="let order of modbusOrderType" [value]="order">{{ order }}</mat-option>
</mat-select>
</mat-form-field>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.byte-order</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="byteOrder">
<mat-option *ngFor="let order of modbusOrderType" [value]="order">{{ order }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.word-order</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="wordOrder">
<mat-option *ngFor="let order of modbusOrderType" [value]="order">{{ order }}</mat-option>
</mat-select>
</mat-form-field>
<div *ngIf="protocolType !== ModbusProtocolType.Serial" class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.word-order</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="wordOrder">
<mat-option *ngFor="let order of modbusOrderType" [value]="order">{{ order }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</div>
<mat-expansion-panel *ngIf="this.slaveConfigFormGroup.get('security')" [expanded]="showSecurityControl.value">
<mat-expansion-panel-header class="nested-expansion-header">
<mat-panel-title>
<mat-slide-toggle [formControl]="showSecurityControl" class="mat-slide" (click)="$event.stopPropagation()">
<mat-label>
{{ 'gateway.tls-connection' | translate }}
</mat-label>
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<tb-modbus-security-config formControlName="security"></tb-modbus-security-config>
</mat-expansion-panel>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="retries">
<mat-label>
{{ 'gateway.retries' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="retryOnEmpty">
<mat-label>
{{ 'gateway.retries-on-empty' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="retryOnInvalid">
<mat-label>
{{ 'gateway.retries-on-invalid' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.poll-period</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="pollPeriod" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
<div *ngIf="protocolType !== ModbusProtocolType.Serial" class="tb-form-panel stroked tb-slide-toggle">
<mat-expansion-panel class="tb-settings" [expanded]="showSecurityControl.value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle fxLayoutAlign="center" [formControl]="showSecurityControl" class="mat-slide" (click)="$event.stopPropagation()">
<mat-label>
{{ 'gateway.tls-connection' | translate }}
</mat-label>
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<tb-modbus-security-config formControlName="security"></tb-modbus-security-config>
</mat-expansion-panel>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.connect-attempt-time</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="connectAttemptTimeMs" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="retries">
<mat-label>
{{ 'gateway.retries' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.connect-attempt-count</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="connectAttemptCount" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="retryOnEmpty">
<mat-label>
{{ 'gateway.retries-on-empty' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.wait-after-failed-attempts</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="waitAfterFailedAttemptsMs" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="retryOnInvalid">
<mat-label>
{{ 'gateway.retries-on-invalid' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width-230" translate>gateway.poll-period</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="pollPeriod" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width-230" translate>gateway.connect-attempt-time</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="connectAttemptTimeMs" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width-230" translate>gateway.connect-attempt-count</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="connectAttemptCount" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width-230" translate>gateway.wait-after-failed-attempts</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="waitAfterFailedAttemptsMs" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
</div>
</div>
</mat-expansion-panel>
</mat-expansion-panel>
</div>
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>gateway.values</div>
<tb-modbus-values [singleMode]="true" formControlName="values"></tb-modbus-values>
</div>
</div>
@ -348,7 +356,7 @@
<button mat-raised-button color="primary"
(click)="add()"
[disabled]="slaveConfigFormGroup.invalid || !slaveConfigFormGroup.dirty">
{{ this.data.buttonTitle | translate }}
{{ data.buttonTitle | translate }}
</button>
</div>
</div>

189
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts

@ -17,21 +17,27 @@
import { ChangeDetectionStrategy, Component, forwardRef, Inject, OnDestroy } from '@angular/core';
import {
FormBuilder,
FormControl,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormControl,
UntypedFormGroup,
Validators,
} from '@angular/forms';
import {
MappingInfo,
ModbusBaudrates,
ModbusByteSizes,
ModbusMethodLabelsMap,
ModbusMethodType,
ModbusOrderType,
ModbusParity,
ModbusParityLabelsMap,
ModbusProtocolLabelsMap,
ModbusProtocolType,
ModbusSerialMethodType,
ModbusSlaveInfo,
noLeadTrailSpacesRegex,
PortLimits, SlaveConfig,
PortLimits,
SlaveConfig,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
@ -43,8 +49,9 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { GatewayPortTooltipPipe } from '@home/pipes/gateway-port-tooltip/gateway-port-tooltip.pipe';
import { GatewayPortTooltipPipe } from '@home/pipes/gateway-port-tooltip.pipe';
import { takeUntil } from 'rxjs/operators';
import { isEqual } from '@core/utils';
@Component({
selector: 'tb-modbus-slave-dialog',
@ -73,33 +80,33 @@ import { takeUntil } from 'rxjs/operators';
styles: [`
:host {
.slaves-config-container {
width: 900px;
}
.nested-expansion-header {
.mat-content {
height: 100%;
overflow: hidden;
}
}
width: 80vw;
max-width: 900px; }
}
`],
})
export class ModbusSlaveDialogComponent extends DialogComponent<ModbusSlaveDialogComponent, SlaveConfig> implements OnDestroy {
slaveConfigFormGroup: UntypedFormGroup;
showSecurityControl: UntypedFormControl;
showSecurityControl: FormControl<boolean>;
portLimits = PortLimits;
readonly modbusProtocolTypes = Object.values(ModbusProtocolType);
readonly modbusMethodTypes = Object.values(ModbusMethodType);
readonly modbusSerialMethodTypes = Object.values(ModbusSerialMethodType);
readonly modbusParities = Object.values(ModbusParity);
readonly modbusByteSizes = ModbusByteSizes;
readonly modbusBaudrates = ModbusBaudrates;
readonly modbusOrderType = Object.values(ModbusOrderType);
readonly ModbusProtocolType = ModbusProtocolType;
readonly ModbusParityLabelsMap = ModbusParityLabelsMap;
readonly ModbusProtocolLabelsMap = ModbusProtocolLabelsMap;
readonly ModbusMethodLabelsMap = ModbusMethodLabelsMap;
readonly modbusHelpLink =
'https://thingsboard.io/docs/iot-gateway/config/modbus/#section-master-description-and-configuration-parameters';
readonly serialSpecificControlKeys = ['serialPort', 'baudrate', 'stopbits', 'bytesize', 'parity', 'strict'];
readonly tcpUdpSpecificControlKeys = ['port', 'security', 'host'];
private readonly serialSpecificControlKeys = ['serialPort', 'baudrate', 'stopbits', 'bytesize', 'parity', 'strict'];
private readonly tcpUdpSpecificControlKeys = ['port', 'security', 'host', 'wordOrder'];
private destroy$ = new Subject<void>();
@ -107,50 +114,85 @@ export class ModbusSlaveDialogComponent extends DialogComponent<ModbusSlaveDialo
private fb: FormBuilder,
protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: MappingInfo,
@Inject(MAT_DIALOG_DATA) public data: ModbusSlaveInfo,
public dialogRef: MatDialogRef<ModbusSlaveDialogComponent, SlaveConfig>,
) {
super(store, router, dialogRef);
this.showSecurityControl = this.fb.control(false);
this.initializeSlaveFormGroup();
this.updateSlaveFormGroup();
this.updateControlsEnabling(this.data.value.type);
this.observeTypeChange();
this.observeShowSecurity();
this.showSecurityControl.patchValue(!!this.data.value.security && !isEqual(this.data.value.security, {}));
}
get protocolType(): ModbusProtocolType {
return this.slaveConfigFormGroup.get('type').value;
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
cancel(): void {
this.dialogRef.close(null);
}
add(): void {
if (!this.slaveConfigFormGroup.valid) {
return;
}
const { values, type, serialPort, ...rest } = this.slaveConfigFormGroup.value;
const slaveResult = { ...rest, type, ...values };
if (type === ModbusProtocolType.Serial) {
slaveResult.port = serialPort;
}
this.dialogRef.close(slaveResult);
}
private initializeSlaveFormGroup(): void {
this.slaveConfigFormGroup = this.fb.group({
name: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
type: [ModbusProtocolType.TCP, [Validators.required]],
type: [ModbusProtocolType.TCP],
host: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]],
serialPort: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
method: [ModbusMethodType.SOCKET, []],
baudrate: [null, []],
stopbits: [null, []],
bytesize: [null, []],
parity: [null, []],
strict: [false, []],
unitId: [null, [Validators.required]],
method: [ModbusMethodType.SOCKET, [Validators.required]],
baudrate: [this.modbusBaudrates[0]],
stopbits: [1],
bytesize: [ModbusByteSizes[0]],
parity: [ModbusParity.None],
strict: [true],
unitId: [0, [Validators.required]],
deviceName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
deviceType: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
sendDataOnlyOnChange: [false, []],
timeout: [],
byteOrder: [ModbusOrderType.BIG, []],
wordOrder: [ModbusOrderType.BIG, []],
retries: [false, []],
retryOnEmpty: [false, []],
retryOnInvalid: [false, []],
pollPeriod: [null, []],
connectAttemptTimeMs: [null, []],
connectAttemptCount: [null, []],
waitAfterFailedAttemptsMs: [null, []],
values: [{}, []],
sendDataOnlyOnChange: [false],
timeout: [35],
byteOrder: [ModbusOrderType.BIG],
wordOrder: [ModbusOrderType.BIG],
retries: [true],
retryOnEmpty: [true],
retryOnInvalid: [true],
pollPeriod: [5000],
connectAttemptTimeMs: [5000],
connectAttemptCount: [5],
waitAfterFailedAttemptsMs: [300000],
values: [{}],
security: [{}],
});
}
private updateSlaveFormGroup(): void {
this.slaveConfigFormGroup.patchValue({
...this.data.value,
port: this.data.value.type === ModbusProtocolType.Serial
? null
: this.data.value.port,
serialPort: this.data.value.type === ModbusProtocolType.Serial
? this.data.value.port
: '',
port: this.data.value.type === ModbusProtocolType.Serial ? null : this.data.value.port,
serialPort: this.data.value.type === ModbusProtocolType.Serial ? this.data.value.port : '',
values: {
attributes: this.data.value.attributes ?? [],
timeseries: this.data.value.timeseries ?? [],
@ -158,45 +200,50 @@ export class ModbusSlaveDialogComponent extends DialogComponent<ModbusSlaveDialo
rpc: this.data.value.rpc ?? [],
}
});
this.showSecurityControl.patchValue(!!this.data.value.security);
this.updateControlsEnabling(this.data.value.type);
this.observeTypeChange();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
private observeTypeChange(): void {
this.slaveConfigFormGroup.get('type').valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(type => {
this.updateControlsEnabling(type);
this.updateMethodType(type);
});
}
cancel(): void {
this.dialogRef.close(null);
private updateMethodType(type: ModbusProtocolType): void {
if (this.slaveConfigFormGroup.get('method').value !== ModbusMethodType.RTU) {
this.slaveConfigFormGroup.get('method').patchValue(
type === ModbusProtocolType.Serial
? ModbusSerialMethodType.ASCII
: ModbusMethodType.SOCKET,
{emitEvent: false}
);
}
}
add(): void {
if (this.slaveConfigFormGroup.valid) {
const slaveResult = {...this.slaveConfigFormGroup.value, ...this.slaveConfigFormGroup.value.values};
delete slaveResult.values;
if (slaveResult.type === ModbusProtocolType.Serial) {
slaveResult.port = slaveResult.serialPort;
}
delete slaveResult.serialPort;
this.dialogRef.close(slaveResult);
}
private updateControlsEnabling(type: ModbusProtocolType): void {
const [enableKeys, disableKeys] = type === ModbusProtocolType.Serial
? [this.serialSpecificControlKeys, this.tcpUdpSpecificControlKeys]
: [this.tcpUdpSpecificControlKeys, this.serialSpecificControlKeys];
enableKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({ emitEvent: false }));
disableKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.disable({ emitEvent: false }));
this.updateSecurityEnabling(this.showSecurityControl.value);
}
private observeTypeChange(): void {
this.slaveConfigFormGroup.get('type').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(type => {
this.updateControlsEnabling(type);
});
private observeShowSecurity(): void {
this.showSecurityControl.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(value => this.updateSecurityEnabling(value));
}
private updateControlsEnabling(type: ModbusProtocolType): void {
if (type === ModbusProtocolType.Serial) {
this.serialSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({emitEvent: false}));
this.tcpUdpSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.disable({emitEvent: false}));
private updateSecurityEnabling(isEnabled: boolean): void {
if (isEnabled && this.protocolType !== ModbusProtocolType.Serial) {
this.slaveConfigFormGroup.get('security').enable({emitEvent: false});
} else {
this.serialSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.disable({emitEvent: false}));
this.tcpUdpSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({emitEvent: false}));
this.slaveConfigFormGroup.get('security').disable({emitEvent: false});
}
};
}
}

16
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.html

@ -36,8 +36,8 @@
<div class="tb-form-row space-between tb-flex">
<div class="fixed-title-width" translate>gateway.attributes</div>
<div class="tb-flex ellipsis-chips-container">
<mat-chip-listbox [tb-ellipsis-chip-list]="getValueGroup(ModbusValueKey.ATTRIBUTES, register)" class="tb-flex">
<mat-chip *ngFor="let attribute of getValueGroup(ModbusValueKey.ATTRIBUTES, register)">
<mat-chip-listbox [tb-ellipsis-chip-list]="getValueGroup(ModbusValueKey.ATTRIBUTES, register).value" class="tb-flex">
<mat-chip *ngFor="let attribute of getValueGroup(ModbusValueKey.ATTRIBUTES, register).value">
{{ attribute.tag }}
</mat-chip>
<mat-chip class="mat-mdc-chip ellipsis-chip">
@ -57,8 +57,8 @@
<div class="tb-form-row space-between tb-flex">
<div class="fixed-title-width" translate>gateway.timeseries</div>
<div class="tb-flex ellipsis-chips-container">
<mat-chip-listbox class="tb-flex" [tb-ellipsis-chip-list]="getValueGroup(ModbusValueKey.TIMESERIES, register)">
<mat-chip *ngFor="let telemetry of getValueGroup(ModbusValueKey.TIMESERIES, register)">
<mat-chip-listbox class="tb-flex" [tb-ellipsis-chip-list]="getValueGroup(ModbusValueKey.TIMESERIES, register).value">
<mat-chip *ngFor="let telemetry of getValueGroup(ModbusValueKey.TIMESERIES, register).value">
{{ telemetry.tag }}
</mat-chip>
<mat-chip class="mat-mdc-chip ellipsis-chip">
@ -78,8 +78,8 @@
<div class="tb-form-row space-between tb-flex">
<div class="fixed-title-width" translate>gateway.attribute-updates</div>
<div class="tb-flex ellipsis-chips-container">
<mat-chip-listbox [tb-ellipsis-chip-list]="getValueGroup(ModbusValueKey.ATTRIBUTES_UPDATES, register)" class="tb-flex">
<mat-chip *ngFor="let attributeUpdate of getValueGroup(ModbusValueKey.ATTRIBUTES_UPDATES, register)">
<mat-chip-listbox [tb-ellipsis-chip-list]="getValueGroup(ModbusValueKey.ATTRIBUTES_UPDATES, register).value" class="tb-flex">
<mat-chip *ngFor="let attributeUpdate of getValueGroup(ModbusValueKey.ATTRIBUTES_UPDATES, register).value">
{{ attributeUpdate.tag }}
</mat-chip>
<mat-chip class="mat-mdc-chip ellipsis-chip">
@ -99,8 +99,8 @@
<div class="tb-form-row space-between tb-flex">
<div class="fixed-title-width" translate>gateway.rpc-requests</div>
<div class="tb-flex ellipsis-chips-container">
<mat-chip-listbox [tb-ellipsis-chip-list]="getValueGroup(ModbusValueKey.RPC_REQUESTS, register)" class="tb-flex">
<mat-chip *ngFor="let rpcRequest of getValueGroup(ModbusValueKey.RPC_REQUESTS, register)">
<mat-chip-listbox [tb-ellipsis-chip-list]="getValueGroup(ModbusValueKey.RPC_REQUESTS, register).value" class="tb-flex">
<mat-chip *ngFor="let rpcRequest of getValueGroup(ModbusValueKey.RPC_REQUESTS, register).value">
{{ rpcRequest.tag }}
</mat-chip>
<mat-chip class="mat-mdc-chip ellipsis-chip">

147
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.ts

@ -20,8 +20,8 @@ import {
Component,
forwardRef,
Input,
OnChanges,
OnDestroy,
OnInit,
Renderer2,
ViewContainerRef
} from '@angular/core';
@ -42,6 +42,7 @@ import {
ModbusRegisterTranslationsMap,
ModbusRegisterType,
ModbusRegisterValues,
ModbusValue,
ModbusValueKey,
ModbusValues,
ModbusValuesState,
@ -50,10 +51,11 @@ import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { EllipsisChipListDirective } from '@shared/directives/public-api';
import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list.directive';
import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import { ModbusDataKeysPanelComponent } from '../modbus-data-keys-panel/modbus-data-keys-panel.component';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-modbus-values',
@ -83,21 +85,26 @@ import { ModbusDataKeysPanelComponent } from '../modbus-data-keys-panel/modbus-d
min-height: 320px;
}
}
::ng-deep .mdc-evolution-chip-set__chips {
align-items: center;
}
`]
})
export class ModbusValuesComponent implements ControlValueAccessor, Validator, OnChanges, OnDestroy {
export class ModbusValuesComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy {
@coerceBoolean()
@Input() singleMode = false;
@Input() disabled = false;
disabled = false;
modbusRegisterTypes: ModbusRegisterType[] = Object.values(ModbusRegisterType);
modbusValueKeys = Object.values(ModbusValueKey);
ModbusValuesTranslationsMap = ModbusRegisterTranslationsMap;
ModbusValueKey = ModbusValueKey;
valuesFormGroup: FormGroup;
private onChange: (value: string) => void;
private onChange: (value: ModbusValuesState) => void;
private onTouched: () => void;
private destroy$ = new Subject<void>();
@ -107,30 +114,19 @@ export class ModbusValuesComponent implements ControlValueAccessor, Validator, O
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef,
private cdr: ChangeDetectorRef,
) {
this.valuesFormGroup = this.fb.group(this.modbusRegisterTypes.reduce((registersAcc, register) => {
return {
...registersAcc,
[register]: this.fb.group(this.modbusValueKeys.reduce((acc, key) => ({...acc, [key]: [[], []]}), {})),
};
}, {}));
) {}
ngOnInit() {
this.initializeValuesFormGroup();
this.observeValuesChanges();
}
ngOnChanges(): void {
if (this.singleMode) {
this.valuesFormGroup = this.fb.group(this.modbusValueKeys.reduce((acc, key) => ({...acc, [key]: [[], []]}), {}));
this.observeValuesChanges();
}
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
registerOnChange(fn: (value: string) => void): void {
registerOnChange(fn: (value: ModbusValuesState) => void): void {
this.onChange = fn;
}
@ -140,15 +136,15 @@ export class ModbusValuesComponent implements ControlValueAccessor, Validator, O
writeValue(values: ModbusValuesState): void {
if (this.singleMode) {
this.valuesFormGroup.setValue(this.getSingleRegisterState(values as ModbusValues), {emitEvent: false});
this.valuesFormGroup.setValue(this.getSingleRegisterState(values as ModbusValues), { emitEvent: false });
} else {
const registers = values as ModbusRegisterValues;
const { holding_registers, coils_initializer, input_registers, discrete_inputs } = values as ModbusRegisterValues;
this.valuesFormGroup.setValue({
holding_registers: this.getSingleRegisterState(registers.holding_registers),
coils_initializer: this.getSingleRegisterState(registers.coils_initializer),
input_registers: this.getSingleRegisterState(registers.input_registers),
discrete_inputs: this.getSingleRegisterState(registers.discrete_inputs),
}, {emitEvent: false});
holding_registers: this.getSingleRegisterState(holding_registers),
coils_initializer: this.getSingleRegisterState(coils_initializer),
input_registers: this.getSingleRegisterState(input_registers),
discrete_inputs: this.getSingleRegisterState(discrete_inputs),
}, { emitEvent: false });
}
this.cdr.markForCheck();
}
@ -159,54 +155,77 @@ export class ModbusValuesComponent implements ControlValueAccessor, Validator, O
};
}
getValueGroup(valueKey: ModbusValueKey, register?: ModbusRegisterType) {
return register ? this.valuesFormGroup.get(register).get(valueKey).value : this.valuesFormGroup.get(valueKey).value;
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
this.cdr.markForCheck();
}
getValueGroup(valueKey: ModbusValueKey, register?: ModbusRegisterType): FormGroup {
return register
? this.valuesFormGroup.get(register).get(valueKey) as FormGroup
: this.valuesFormGroup.get(valueKey) as FormGroup;
}
manageKeys($event: Event, matButton: MatButton, keysType: ModbusValueKey, register?: ModbusRegisterType): void {
if ($event) {
$event.stopPropagation();
}
$event.stopPropagation();
const trigger = matButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
return;
}
const keysControl = this.getValueGroup(keysType, register);
const ctx = {
values: keysControl.value,
isMaster: !this.singleMode,
keysType,
panelTitle: ModbusKeysPanelTitleTranslationsMap.get(keysType),
addKeyTitle: ModbusKeysAddKeyTranslationsMap.get(keysType),
deleteKeyTitle: ModbusKeysDeleteKeyTranslationsMap.get(keysType),
noKeysText: ModbusKeysNoKeysTextTranslationsMap.get(keysType)
};
const dataKeysPanelPopover = this.popoverService.displayPopover(
trigger,
this.renderer,
this.viewContainerRef,
ModbusDataKeysPanelComponent,
'leftBottom',
false,
null,
ctx,
{},
{},
{},
true
);
dataKeysPanelPopover.tbComponentRef.instance.popover = dataKeysPanelPopover;
dataKeysPanelPopover.tbComponentRef.instance.keysDataApplied.pipe(takeUntil(this.destroy$)).subscribe((keysData: ModbusValue[]) => {
dataKeysPanelPopover.hide();
keysControl.patchValue(keysData);
keysControl.markAsDirty();
this.cdr.markForCheck();
});
}
private initializeValuesFormGroup(): void {
const getValuesFormGroup = () => this.fb.group(this.modbusValueKeys.reduce((acc, key) => {
acc[key] = this.fb.control([[], []]);
return acc;
}, {}));
if (this.singleMode) {
this.valuesFormGroup = getValuesFormGroup();
} else {
const group = this.valuesFormGroup;
const keysControl = register ? group.get(register).get(keysType) : group.get(keysType);
const ctx = {
values: keysControl.value,
isMaster: !this.singleMode,
keysType,
panelTitle: ModbusKeysPanelTitleTranslationsMap.get(keysType),
addKeyTitle: ModbusKeysAddKeyTranslationsMap.get(keysType),
deleteKeyTitle: ModbusKeysDeleteKeyTranslationsMap.get(keysType),
noKeysText: ModbusKeysNoKeysTextTranslationsMap.get(keysType)
};
const dataKeysPanelPopover = this.popoverService.displayPopover(
trigger,
this.renderer,
this.viewContainerRef,
ModbusDataKeysPanelComponent,
'leftBottom',
false,
null,
ctx,
{},
{},
{},
true
this.valuesFormGroup = this.fb.group(
this.modbusRegisterTypes.reduce((registersAcc, register) => {
registersAcc[register] = getValuesFormGroup();
return registersAcc;
}, {})
);
dataKeysPanelPopover.tbComponentRef.instance.popover = dataKeysPanelPopover;
dataKeysPanelPopover.tbComponentRef.instance.keysDataApplied.pipe(takeUntil(this.destroy$)).subscribe((keysData) => {
dataKeysPanelPopover.hide();
keysControl.patchValue(keysData);
keysControl.markAsDirty();
this.cdr.markForCheck();
});
}
}
private observeValuesChanges(): void {
this.valuesFormGroup.valueChanges
.pipe(takeUntil(this.destroy$))

23
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/public-api.ts

@ -1,23 +0,0 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
export * from './modbus-basic-config/modbus-basic-config.component';
export * from './modbus-values/modbus-values.component';
export * from './modbus-data-keys-panel/modbus-data-keys-panel.component';
export * from './modbus-slave-config/modbus-slave-config.component';
export * from './modbus-master-table/modbus-master-table.component';
export * from './modbus-slave-dialog/modbus-slave-dialog.component';
export * from './modbus-security-config/modbus-security-config.component';

18
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts

@ -32,15 +32,21 @@ import {
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import {
BrokerConfigControlComponent,
MappingTableComponent,
SecurityConfigComponent,
WorkersConfigControlComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/public-api';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { isObject } from 'lodash';
import {
SecurityConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component';
import {
WorkersConfigControlComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component';
import {
BrokerConfigControlComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component';
import {
MappingTableComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component';
@Component({
selector: 'tb-mqtt-basic-config',

4
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts

@ -33,10 +33,12 @@ import {
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { SecurityConfigComponent } from '@home/components/widget/lib/gateway/connectors-configuration/public-api';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { TruncateTooltipDirective } from '@shared/directives/truncate-tooltip.directive';
import {
SecurityConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component';
@Component({
selector: 'tb-opc-server-config',

22
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-ua-basic-config/opc-ua-basic-config.component.ts

@ -31,15 +31,23 @@ import {
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import {
BrokerConfigControlComponent,
MappingTableComponent,
SecurityConfigComponent,
OpcServerConfigComponent,
WorkersConfigControlComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/public-api';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import {
SecurityConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component';
import {
WorkersConfigControlComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component';
import {
BrokerConfigControlComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component';
import {
MappingTableComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component';
import {
OpcServerConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component';
@Component({
selector: 'tb-opc-ua-basic-config',

27
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/public-api.ts

@ -1,27 +0,0 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
export * from './mapping-table/mapping-table.component';
export * from './device-info-table/device-info-table.component';
export * from './security-config/security-config.component';
export * from './opc-server-config/opc-server-config.component';
export * from './mapping-data-keys-panel/mapping-data-keys-panel.component';
export * from './type-value-panel/type-value-panel.component';
export * from './broker-config-control/broker-config-control.component';
export * from './workers-config-control/workers-config-control.component';
export * from './opc-ua-basic-config/opc-ua-basic-config.component';
export * from './mqtt-basic-config/mqtt-basic-config.component';
export * from './modbus/public-api';

4
ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts

@ -60,7 +60,9 @@ import { startWith, takeUntil } from 'rxjs/operators';
import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import { TranslateService } from '@ngx-translate/core';
import { MappingDataKeysPanelComponent } from '@home/components/widget/lib/gateway/connectors-configuration/public-api';
import {
MappingDataKeysPanelComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel/mapping-data-keys-panel.component';
@Component({
selector: 'tb-mapping-dialog',

77
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts

@ -493,6 +493,11 @@ export interface MappingInfo {
buttonTitle: string;
}
export interface ModbusSlaveInfo {
value: SlaveConfig;
buttonTitle: string;
}
export enum ConnectorConfigurationModes {
BASIC = 'basic',
ADVANCED = 'advanced'
@ -789,10 +794,32 @@ export enum ModbusMethodType {
RTU = 'rtu',
}
export const ModbusMethodLabelsMap = new Map<ModbusMethodType, string>(
export enum ModbusSerialMethodType {
RTU = 'rtu',
ASCII = 'ascii',
}
export const ModbusMethodLabelsMap = new Map<ModbusMethodType | ModbusSerialMethodType, string>(
[
[ModbusMethodType.SOCKET, 'Socket'],
[ModbusMethodType.RTU, 'RTU'],
[ModbusSerialMethodType.ASCII, 'ASCII'],
]
);
export const ModbusByteSizes = [5, 6, 7 ,8];
export enum ModbusParity {
Even = 'E',
Odd = 'O',
None = 'N'
}
export const ModbusParityLabelsMap = new Map<ModbusParity, string>(
[
[ModbusParity.Even, 'Even'],
[ModbusParity.Odd, 'Odd'],
[ModbusParity.None, 'None'],
]
);
@ -802,17 +829,17 @@ export enum ModbusOrderType {
}
export enum ModbusRegisterType {
HoldingRegister = 'holding_registers',
HoldingRegisters = 'holding_registers',
CoilsInitializer = 'coils_initializer',
InputRegister = 'input_registers',
InputRegisters = 'input_registers',
DiscreteInputs = 'discrete_inputs'
}
export const ModbusRegisterTranslationsMap = new Map<ModbusRegisterType, string>(
[
[ModbusRegisterType.HoldingRegister, 'gateway.holding_registers'],
[ModbusRegisterType.HoldingRegisters, 'gateway.holding_registers'],
[ModbusRegisterType.CoilsInitializer, 'gateway.coils_initializer'],
[ModbusRegisterType.InputRegister, 'gateway.input_registers'],
[ModbusRegisterType.InputRegisters, 'gateway.input_registers'],
[ModbusRegisterType.DiscreteInputs, 'gateway.discrete_inputs']
]
);
@ -821,6 +848,9 @@ export enum ModbusDataType {
STRING = 'string',
BYTES = 'bytes',
BITS = 'bits',
INT8 = '8int',
UINT8 = '8uint',
FLOAT8 = '8float',
INT16 = '16int',
UINT16 = '16uint',
FLOAT16 = '16float',
@ -833,6 +863,9 @@ export enum ModbusDataType {
}
export enum ModbusObjectCountByDataType {
'8int' = 1,
'8uint' = 1,
'8float' = 1,
'16int' = 1,
'16uint' = 1,
'16float' = 1,
@ -844,24 +877,6 @@ export enum ModbusObjectCountByDataType {
'64float' = 4,
}
export enum ModbusValueField {
Tag = 'tag',
Type = 'type',
ObjectsCount = 'objectsCount',
Address = 'address',
Value = 'value',
}
export const ModbusFieldsTranslationsMap = new Map<ModbusValueField, string>(
[
[ModbusValueField.Tag, 'gateway.tag'],
[ModbusValueField.Type, 'gateway.type'],
[ModbusValueField.ObjectsCount, 'gateway.objects_count'],
[ModbusValueField.Address, 'gateway.address'],
[ModbusValueField.Value, 'gateway.value']
]
);
export enum ModbusValueKey {
ATTRIBUTES = 'attributes',
TIMESERIES = 'timeseries',
@ -892,7 +907,7 @@ export const ModbusKeysDeleteKeyTranslationsMap = new Map<ModbusValueKey, string
[ModbusValueKey.ATTRIBUTES, 'gateway.delete-attribute'],
[ModbusValueKey.TIMESERIES, 'gateway.delete-timeseries'],
[ModbusValueKey.ATTRIBUTES_UPDATES, 'gateway.delete-attribute-update'],
[ModbusValueKey.RPC_REQUESTS, 'gateway.delete-rpc-requests']
[ModbusValueKey.RPC_REQUESTS, 'gateway.delete-rpc-request']
]
);
@ -905,14 +920,6 @@ export const ModbusKeysNoKeysTextTranslationsMap = new Map<ModbusValueKey, strin
]
);
export const ModbusClientTypeLabelsMap = new Map<ModbusProtocolType, string>(
[
[ModbusProtocolType.TCP, 'TCP/UDP'],
[ModbusProtocolType.UDP, 'TCP/UDP'],
[ModbusProtocolType.Serial, 'Serial'],
]
);
export const ModbusFunctionCodeTranslationsMap = new Map<number, string>(
[
[1, 'gateway.read-coils'],
@ -946,7 +953,7 @@ export interface SlaveConfig {
pollPeriod: number;
unitId: number;
deviceName: string;
deviceType?: string;
deviceType: string;
sendDataOnlyOnChange: boolean;
connectAttemptTimeMs: number;
connectAttemptCount: number;
@ -959,7 +966,7 @@ export interface SlaveConfig {
baudrate?: number;
stopbits?: number;
bytesize?: number;
parity?: string;
parity?: ModbusParity;
strict?: boolean;
}
@ -1021,3 +1028,5 @@ export interface ModbusIdentity {
productName?: string;
modelName?: string;
}
export const ModbusBaudrates = [4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600];

81
ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts

@ -109,20 +109,38 @@ import {
import {
NotificationTypeFilterPanelComponent
} from '@home/components/widget/lib/cards/notification-type-filter-panel.component';
import { GatewayHelpLinkPipe } from '@home/pipes/public-api';
import { GatewayHelpLinkPipe } from '@home/pipes/gateway-help-link.pipe';
import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list.directive';
import {
BrokerConfigControlComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component';
import {
WorkersConfigControlComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component';
import {
OpcServerConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component';
import {
MqttBasicConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component';
import {
MappingTableComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component';
import {
OpcUaBasicConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/opc-ua-basic-config/opc-ua-basic-config.component';
import {
BrokerConfigControlComponent,
DeviceInfoTableComponent,
MappingDataKeysPanelComponent,
MappingTableComponent,
MqttBasicConfigComponent,
OpcUaBasicConfigComponent,
OpcServerConfigComponent,
TypeValuePanelComponent,
WorkersConfigControlComponent,
ModbusBasicConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/public-api';
import { EllipsisChipListDirective } from '@shared/directives/public-api';
} from '@home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component';
import {
DeviceInfoTableComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/device-info-table/device-info-table.component';
import {
MappingDataKeysPanelComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel/mapping-data-keys-panel.component';
import {
TypeValuePanelComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component';
@NgModule({
declarations: [
@ -190,25 +208,26 @@ import { EllipsisChipListDirective } from '@shared/directives/public-api';
LabelCardWidgetComponent,
LabelValueCardWidgetComponent,
UnreadNotificationWidgetComponent,
NotificationTypeFilterPanelComponent],
imports: [
CommonModule,
SharedModule,
RpcWidgetsModule,
HomePageWidgetsModule,
SharedHomeComponentsModule,
RestConnectorSecurityComponent,
GatewayHelpLinkPipe,
BrokerConfigControlComponent,
WorkersConfigControlComponent,
OpcServerConfigComponent,
MqttBasicConfigComponent,
MappingTableComponent,
OpcUaBasicConfigComponent,
KeyValueIsNotEmptyPipe,
ModbusBasicConfigComponent,
EllipsisChipListDirective,
],
NotificationTypeFilterPanelComponent
],
imports: [
CommonModule,
SharedModule,
RpcWidgetsModule,
HomePageWidgetsModule,
SharedHomeComponentsModule,
RestConnectorSecurityComponent,
GatewayHelpLinkPipe,
BrokerConfigControlComponent,
WorkersConfigControlComponent,
OpcServerConfigComponent,
MqttBasicConfigComponent,
MappingTableComponent,
OpcUaBasicConfigComponent,
KeyValueIsNotEmptyPipe,
ModbusBasicConfigComponent,
EllipsisChipListDirective,
],
exports: [
EntitiesTableWidgetComponent,
AlarmsTableWidgetComponent,

0
ui-ngx/src/app/modules/home/pipes/gateway-help-link/gateway-help-link.pipe.ts → ui-ngx/src/app/modules/home/pipes/gateway-help-link.pipe.ts

14
ui-ngx/src/app/modules/home/pipes/gateway-port-tooltip/gateway-port-tooltip.pipe.ts → ui-ngx/src/app/modules/home/pipes/gateway-port-tooltip.pipe.ts

@ -15,7 +15,7 @@
///
import { Pipe, PipeTransform } from '@angular/core';
import { PortLimits, } from '@home/components/widget/lib/gateway/gateway-widget.models';
import { PortLimits } from '@home/components/widget/lib/gateway/gateway-widget.models';
import { AbstractControl } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
@ -30,12 +30,12 @@ export class GatewayPortTooltipPipe implements PipeTransform {
transform(portControl: AbstractControl): string {
if (portControl.hasError('required')) {
return this.translate.instant('gateway.port-required');
} else if (
portControl.hasError('min') ||
portControl.hasError('max')
) {
return this.translate.instant('gateway.port-limits-error',
{min: PortLimits.MIN, max: PortLimits.MAX});
}
if (portControl.hasError('min') || portControl.hasError('max')) {
return this.translate.instant('gateway.port-limits-error', {
min: PortLimits.MIN,
max: PortLimits.MAX,
});
}
return '';
}

18
ui-ngx/src/app/modules/home/pipes/public-api.ts

@ -1,18 +0,0 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
export * from './gateway-help-link/gateway-help-link.pipe';
export * from './gateway-port-tooltip/gateway-port-tooltip.pipe';

17
ui-ngx/src/app/shared/abstract/public-api.ts

@ -1,17 +0,0 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
export * from './datasource/datasource.abstract';

5
ui-ngx/src/app/shared/directives/ellipsis-chip-list/ellipsis-chip-list.directive.ts → ui-ngx/src/app/shared/directives/ellipsis-chip-list.directive.ts

@ -61,8 +61,11 @@ export class EllipsisChipListDirective implements OnDestroy {
).subscribe(() => {
this.adjustChips();
});
this.observeIntersection();
}
this.intersectionObserver = new IntersectionObserver((entries) => {
private observeIntersection(): void {
this.intersectionObserver = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.adjustChips();

15
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -2756,6 +2756,7 @@
},
"gateway": {
"address": "Address",
"address-required": "Address required",
"add-entry": "Add configuration",
"add-attribute": "Add attribute",
"add-attribute-update": "Add attribute update",
@ -2772,7 +2773,7 @@
"delete-value": "Delete value",
"delete-rpc-method": "Delete method",
"delete-rpc-request": "Delete request",
"delete-attribute-update": "Add attribute update",
"delete-attribute-update": "Delete attribute update",
"advanced": "Advanced",
"advanced-connection-settings": "Advanced connection settings",
"attributes": "Attributes",
@ -2821,7 +2822,7 @@
"connectors-table-key": "Key",
"connectors-table-class": "Class",
"connection-timeout": "Connection timeout (s)",
"connect-attempt-time": "Connect attempt time (s)",
"connect-attempt-time": "Connect attempt time (ms)",
"connect-attempt-count": "Connect attempt count",
"copy-username": "Copy username",
"copy-password": "Copy password",
@ -2954,10 +2955,10 @@
"unit-id": "Unit ID",
"host": "Host",
"host-required": "Host is required.",
"holding_registers": "Holding register",
"holding_registers": "Holding registers",
"coils_initializer": "Coils initializer",
"input_registers": "Input register",
"discrete_inputs": "Discrete input",
"input_registers": "Input registers",
"discrete_inputs": "Discrete inputs",
"json-parse": "Not valid JSON.",
"json-required": "Field cannot be empty.",
"JSONPath-hint": "This field supports constants and JSONPath expressions.",
@ -3019,7 +3020,7 @@
"password": "Password",
"password-required": "Password is required.",
"permit-without-calls": "Keep alive permit without calls",
"poll-period": "Poll period (s)",
"poll-period": "Poll period (ms)",
"port": "Port",
"port-required": "Port is required.",
"port-limits-error": "Port should be number from {{min}} to {{max}}.",
@ -3308,7 +3309,7 @@
"exactly-once": "2 - Exactly once"
},
"objects-count": "Objects count",
"wait-after-failed-attempts": "Wait after failed attempts (s)",
"wait-after-failed-attempts": "Wait after failed attempts (ms)",
"tls-path-private-key": "Path to private key on gateway",
"toggle-fullscreen": "Toggle fullscreen",
"transformer-json-config": "Configuration JSON*",

3
ui-ngx/src/assets/metadata/connector-default-configs/modbus.json

@ -16,6 +16,7 @@
"pollPeriod": 5000,
"unitId": 1,
"deviceName": "Temp Sensor",
"deviceType": "default",
"sendDataOnlyOnChange": true,
"connectAttemptTimeMs": 5000,
"connectAttemptCount": 5,
@ -199,7 +200,7 @@
"timeseries": [
{
"address": 2,
"type": "int",
"type": "8int",
"tag": "smm",
"objectsCount": 1,
"value": "12334"

Loading…
Cancel
Save