diff --git a/hertzbeat-collector/hertzbeat-collector-basic/pom.xml b/hertzbeat-collector/hertzbeat-collector-basic/pom.xml
index 9f862f5005a..42a8fb6e8cf 100644
--- a/hertzbeat-collector/hertzbeat-collector-basic/pom.xml
+++ b/hertzbeat-collector/hertzbeat-collector-basic/pom.xml
@@ -144,5 +144,16 @@
hivemq-mqtt-client
${mqtt.version}
+
+
+ org.apache.plc4x
+ plc4j-api
+ 0.12.0
+
+
+ org.apache.plc4x
+ plc4j-driver-modbus
+ 0.12.0
+
\ No newline at end of file
diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/modbus/ModbusCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/modbus/ModbusCollectImpl.java
new file mode 100644
index 00000000000..b51f8b5da15
--- /dev/null
+++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/modbus/ModbusCollectImpl.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package org.apache.hertzbeat.collector.collect.modbus;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.hertzbeat.collector.collect.plc.AbstractPlcCollectImpl;
+import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
+import org.apache.hertzbeat.common.entity.job.Metrics;
+import org.apache.hertzbeat.common.entity.job.protocol.ModbusProtocol;
+import org.apache.hertzbeat.common.entity.job.protocol.PlcProtocol;
+import org.apache.hertzbeat.common.entity.message.CollectRep;
+import org.apache.plc4x.java.api.PlcConnection;
+import org.apache.plc4x.java.api.messages.PlcReadRequest;
+import org.springframework.beans.BeanUtils;
+import org.springframework.util.StringUtils;
+
+import java.util.List;
+
+/**
+ * plc collect
+ */
+@Slf4j
+public class ModbusCollectImpl extends AbstractPlcCollectImpl {
+
+ @Override
+ public void preCheck(Metrics metrics) throws IllegalArgumentException {
+ ModbusProtocol modbus = metrics.getModbus();
+ List registerAddressList = modbus.getRegisterAddresses();
+ // check slaveId
+ if (!StringUtils.hasText(modbus.getSlaveId())) {
+ modbus.setSlaveId("1");
+ }
+ if (!StringUtils.hasText(modbus.getTimeout())) {
+ modbus.setTimeout("5000");
+ }
+ PlcProtocol plc = metrics.getPlc() == null ? new PlcProtocol() : metrics.getPlc();
+ plc.setRegisterAddresses(registerAddressList);
+ BeanUtils.copyProperties(modbus, plc);
+ metrics.setPlc(plc);
+ super.preCheck(metrics);
+ }
+
+ @Override
+ public void collect(CollectRep.MetricsData.Builder builder, long monitorId, String app, Metrics metrics) {
+ super.collect(builder, monitorId, app, metrics);
+ }
+
+ @Override
+ public String supportProtocol() {
+ return DispatchConstants.PROTOCOL_MODBUS;
+ }
+
+ @Override
+ protected String getConnectionString(Metrics metrics) {
+ ModbusProtocol plcProtocol = metrics.getModbus();
+ return "modbus-tcp:tcp://" + plcProtocol.getHost() + ":" + plcProtocol.getPort() + "?unit-identifier=" + plcProtocol.getSlaveId();
+ }
+
+ @Override
+ protected PlcReadRequest buildRequest(Metrics metrics, PlcConnection connection) {
+ ModbusProtocol modbus = metrics.getModbus();
+ List registerAddressList = modbus.getRegisterAddresses();
+ // Create a new read request:
+ PlcReadRequest.Builder requestBuilder = connection.readRequestBuilder();
+ for (int i = 0; i < registerAddressList.size(); i++) {
+ String s1 = modbus.getAddressSyntax() + ":" + registerAddressList.get(i);
+ requestBuilder.addTagAddress(metrics.getModbus().getAddressSyntax() + ":" + i, s1);
+ }
+ return requestBuilder.build();
+ }
+}
\ No newline at end of file
diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/plc/AbstractPlcCollectImpl.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/plc/AbstractPlcCollectImpl.java
new file mode 100644
index 00000000000..201a6321651
--- /dev/null
+++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/plc/AbstractPlcCollectImpl.java
@@ -0,0 +1,182 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package org.apache.hertzbeat.collector.collect.plc;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.hertzbeat.collector.collect.AbstractCollect;
+import org.apache.hertzbeat.collector.constants.CollectorConstants;
+import org.apache.hertzbeat.common.constants.CommonConstants;
+import org.apache.hertzbeat.common.entity.job.Metrics;
+import org.apache.hertzbeat.common.entity.job.protocol.PlcProtocol;
+import org.apache.hertzbeat.common.entity.message.CollectRep;
+import org.apache.hertzbeat.common.util.CommonUtil;
+import org.apache.plc4x.java.api.PlcConnection;
+import org.apache.plc4x.java.api.PlcConnectionManager;
+import org.apache.plc4x.java.api.PlcDriverManager;
+import org.apache.plc4x.java.api.messages.PlcReadRequest;
+import org.apache.plc4x.java.api.messages.PlcReadResponse;
+import org.apache.plc4x.java.api.types.PlcResponseCode;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+@Slf4j
+public abstract class AbstractPlcCollectImpl extends AbstractCollect {
+ private static final String[] DRIVER_LIST = {"s7", "modbus-tcp"};
+ private static final String[] ADDRESS_SYNTAX = {"discrete-input", "coil", "input-register", "holding-register"};
+ private static final String COIL = "coil";
+
+ private static final PlcConnectionManager CONNECTION_MANAGER;
+
+ static {
+ CONNECTION_MANAGER = PlcDriverManager.getDefault().getConnectionManager();
+ }
+
+
+ @Override
+ public void preCheck(Metrics metrics) throws IllegalArgumentException {
+ if (metrics == null || metrics.getPlc() == null) {
+ throw new IllegalArgumentException("PLC collect must have PLC params");
+ }
+ // check driver name
+ if (metrics.getPlc().getDriverName() == null || !ArrayUtils.contains(DRIVER_LIST, metrics.getPlc().getDriverName())) {
+ throw new IllegalArgumentException("PLC collect must have valid driver name");
+ }
+ // check address syntax
+ if (!ArrayUtils.contains(ADDRESS_SYNTAX, metrics.getPlc().getAddressSyntax())) {
+ throw new IllegalArgumentException("PLC collect must have valid address syntax");
+ }
+ // check register address
+ if (metrics.getPlc().getRegisterAddresses() == null || metrics.getPlc().getRegisterAddresses().isEmpty()) {
+ throw new IllegalArgumentException("PLC collect must have register address");
+ }
+ // check timeout is legal
+ if (Objects.nonNull(metrics.getPlc().getTimeout())) {
+ try {
+ Long.parseLong(metrics.getPlc().getTimeout());
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException("PLC collect must have valid timeout");
+ }
+ }
+
+ AtomicInteger addressCount = new AtomicInteger();
+ metrics.getPlc().getRegisterAddresses().forEach(address -> {
+ if (address.contains("[") && address.contains("]")) {
+ String[] addressArray = address.split("\\[");
+ String num = addressArray[1].replace("]", "");
+ addressCount.addAndGet(Integer.parseInt(num));
+ } else {
+ addressCount.addAndGet(1);
+ }
+ });
+ List aliasFields = metrics.getAliasFields();
+ if (Objects.isNull(aliasFields)) {
+ throw new IllegalArgumentException("Please ensure that the number of aliasFields (tagName) in yml matches the number of registered addresses"
+ + "Number of AliasFields(tagList): 0 ,but Number of addresses:"
+ + addressCount.get());
+ }
+ int tagListCount = aliasFields.size() - 1;
+ if (aliasFields.size() - 1 != addressCount.get()) {
+ throw new IllegalArgumentException("Please ensure that the number of aliasFields (tagName) in yml matches the number of registered addresses"
+ + "Number of AliasFields(tagList): "
+ + tagListCount
+ + " ,but Number of addresses:" + addressCount.get());
+ }
+ }
+
+ @Override
+ public void collect(CollectRep.MetricsData.Builder builder, long monitorId, String app, Metrics metrics) {
+
+ long startTime = System.currentTimeMillis();
+ PlcProtocol plcProtocol = metrics.getPlc();
+ PlcConnection plcConnection = null;
+ try {
+ String connectionString = getConnectionString(metrics);
+ plcConnection = CONNECTION_MANAGER.getConnection(connectionString);
+ if (!plcConnection.getMetadata().isReadSupported()) {
+ log.error("This connection doesn't support reading.");
+ }
+ // Check if this connection support reading of data.
+ if (!plcConnection.getMetadata().isWriteSupported()) {
+ log.error("This connection doesn't support writing.");
+ }
+
+ PlcReadRequest readRequest = buildRequest(metrics, plcConnection);
+ PlcReadResponse response = readRequest.execute().get(Long.parseLong(plcProtocol.getTimeout()), TimeUnit.MILLISECONDS);
+ long responseTime = System.currentTimeMillis() - startTime;
+ Map resultMap = new HashMap<>();
+ for (String tagName : response.getTagNames()) {
+ if (response.getResponseCode(tagName) == PlcResponseCode.OK) {
+ int numValues = response.getNumberOfValues(tagName);
+ // If it's just one element, output just one single line.
+ log.info("{}: {}", tagName, response.getPlcValue(tagName));
+ if (numValues == 1) {
+ resultMap.put(tagName, response.getPlcValue(tagName).toString());
+ }
+ // If it's more than one element, output each in a single row.
+ else {
+ for (int i = 0; i < numValues; i++) {
+ resultMap.put(tagName + "-" + i, response.getObject(tagName, i).toString());
+ }
+ }
+ } else {
+ log.error("Error[{}]: {}", tagName, response.getResponseCode(tagName).name());
+ }
+ }
+ if (COIL.equals(plcProtocol.getAddressSyntax())) {
+ resultMap = resultMap.entrySet()
+ .stream()
+ .peek(obj -> obj.setValue(String.valueOf(Boolean.TRUE.equals(Boolean.valueOf(obj.getValue())) ? 1 : 0)))
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+
+ }
+ resultMap.put(CollectorConstants.RESPONSE_TIME, Long.toString(responseTime));
+ List aliasFields = metrics.getAliasFields();
+ CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
+ for (String field : aliasFields) {
+ String fieldValue = resultMap.get(field);
+ valueRowBuilder.addColumns(Objects.requireNonNullElse(fieldValue, CommonConstants.NULL_VALUE));
+ }
+ builder.addValues(valueRowBuilder.build());
+ } catch (Exception e) {
+ builder.setCode(CollectRep.Code.FAIL);
+ String message = CommonUtil.getMessageFromThrowable(e);
+ builder.setMsg(message);
+ log.warn(message, e);
+ } finally {
+ if (plcConnection != null) {
+ try {
+ plcConnection.close();
+ } catch (Exception e) {
+ log.warn(e.getMessage());
+ }
+ }
+ }
+
+ }
+
+ protected abstract String getConnectionString(Metrics metrics);
+
+ protected abstract PlcReadRequest buildRequest(Metrics metrics, PlcConnection connection);
+}
\ No newline at end of file
diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/modbus/ModbusCollectTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/modbus/ModbusCollectTest.java
new file mode 100644
index 00000000000..53dc74ab8b5
--- /dev/null
+++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/modbus/ModbusCollectTest.java
@@ -0,0 +1,227 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package org.apache.hertzbeat.collector.collect.modbus;
+
+
+import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
+import org.apache.hertzbeat.common.entity.job.Metrics;
+import org.apache.hertzbeat.common.entity.job.protocol.ModbusProtocol;
+import org.apache.hertzbeat.common.entity.message.CollectRep;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Test case for {@link ModbusCollectImpl}
+ */
+public class ModbusCollectTest {
+ private ModbusCollectImpl modbusCollect;
+ private Metrics metrics;
+ private CollectRep.MetricsData.Builder builder;
+
+ @BeforeEach
+ public void setup() {
+ modbusCollect = new ModbusCollectImpl();
+ ModbusProtocol modbus = ModbusProtocol.builder().build();
+ metrics = Metrics.builder()
+ .modbus(modbus)
+ .build();
+ builder = CollectRep.MetricsData.newBuilder();
+ }
+
+ @Test
+ void preCheck() {
+ // host is empty
+ assertThrows(IllegalArgumentException.class, () -> {
+ modbusCollect.preCheck(metrics);
+ });
+
+ // port is empty default add 502
+ assertThrows(IllegalArgumentException.class, () -> {
+ ModbusProtocol modbus = ModbusProtocol.builder().build();
+ modbus.setHost("127.0.0.1");
+ metrics.setModbus(modbus);
+ modbusCollect.preCheck(metrics);
+ });
+
+ // driverName version is empty
+ assertThrows(IllegalArgumentException.class, () -> {
+ ModbusProtocol modbus = ModbusProtocol.builder().build();
+ modbus.setHost("127.0.0.1");
+ modbus.setPort("502");
+ metrics.setModbus(modbus);
+ modbusCollect.preCheck(metrics);
+ });
+
+ // addressSyntax is empty
+ assertThrows(IllegalArgumentException.class, () -> {
+ ModbusProtocol modbus = ModbusProtocol.builder().build();
+ modbus.setHost("127.0.0.1");
+ modbus.setPort("502");
+ modbus.setDriverName("modbus-tcp");
+ metrics.setModbus(modbus);
+ modbusCollect.preCheck(metrics);
+ });
+
+ // registerAddresses version is empty
+ assertThrows(IllegalArgumentException.class, () -> {
+ ModbusProtocol modbus = ModbusProtocol.builder().build();
+ modbus.setHost("127.0.0.1");
+ modbus.setPort("502");
+ modbus.setDriverName("modbus-tcp");
+ modbus.setAddressSyntax("holding-register");
+ metrics.setModbus(modbus);
+ modbusCollect.preCheck(metrics);
+ });
+
+ // aliasFields is empty
+ assertThrows(IllegalArgumentException.class, () -> {
+ ModbusProtocol modbus = ModbusProtocol.builder().build();
+ modbus.setHost("127.0.0.1");
+ modbus.setPort("502");
+ modbus.setDriverName("modbus-tcp");
+ modbus.setAddressSyntax("holding-register");
+ modbus.setRegisterAddresses(List.of("1"));
+ metrics.setModbus(modbus);
+ modbusCollect.preCheck(metrics);
+ });
+
+ // the number of aliasFields is not equal to registerAddresses(responseTime doesn't count)
+ assertThrows(IllegalArgumentException.class, () -> {
+ ModbusProtocol modbus = ModbusProtocol.builder().build();
+ modbus.setHost("127.0.0.1");
+ modbus.setPort("502");
+ modbus.setDriverName("modbus-tcp");
+ modbus.setAddressSyntax("holding-register");
+ modbus.setRegisterAddresses(List.of("1", "2"));
+ metrics.setAliasFields(List.of(
+ "responseTime",
+ "holding-register:0"
+ ));
+ metrics.setModbus(modbus);
+ modbusCollect.preCheck(metrics);
+ });
+
+ // everything is ok
+ assertDoesNotThrow(() -> {
+ ModbusProtocol modbus = ModbusProtocol.builder().build();
+ modbus.setHost("127.0.0.1");
+ modbus.setPort("502");
+ modbus.setDriverName("modbus-tcp");
+ modbus.setAddressSyntax("holding-register");
+ modbus.setRegisterAddresses(List.of("1", "2", "3[2]"));
+ modbus.setSlaveId("2");
+ modbus.setTimeout("500");
+ metrics.setModbus(modbus);
+ metrics.setAliasFields(List.of(
+ "responseTime",
+ "holding-register:0",
+ "holding-register:1",
+ "holding-register:2-0",
+ "holding-register:2-1"
+ ));
+ modbusCollect.preCheck(metrics);
+ });
+ }
+
+ @Test
+ void supportProtocol() {
+ Assertions.assertEquals(DispatchConstants.PROTOCOL_MODBUS, modbusCollect.supportProtocol());
+ }
+
+ @Test
+ void collect() {
+ // with holding-register
+ assertDoesNotThrow(() -> {
+ ModbusProtocol modbus = ModbusProtocol.builder().build();
+ modbus.setHost("127.0.0.1");
+ modbus.setPort("502");
+ modbus.setDriverName("modbus-tcp");
+ modbus.setAddressSyntax("holding-register");
+ modbus.setRegisterAddresses(List.of("1", "2[3]"));
+ modbus.setSlaveId("1");
+ modbus.setTimeout("500");
+
+ metrics.setModbus(modbus);
+ metrics.setAliasFields(List.of(
+ "responseTime",
+ "holding-register:0",
+ "holding-register:1-0",
+ "holding-register:1-1",
+ "holding-register:1-2"
+ ));
+ modbusCollect.preCheck(metrics);
+ modbusCollect.collect(builder, 1L, "app", metrics);
+ });
+
+ // with coil
+ assertDoesNotThrow(() -> {
+ ModbusProtocol modbus = ModbusProtocol.builder().build();
+ modbus.setHost("127.0.0.1");
+ modbus.setPort("502");
+ modbus.setDriverName("modbus-tcp");
+ modbus.setAddressSyntax("coil");
+ modbus.setRegisterAddresses(List.of("1", "2[3]"));
+ modbus.setSlaveId("1");
+ modbus.setTimeout("500");
+
+ metrics.setModbus(modbus);
+ metrics.setAliasFields(List.of(
+ "responseTime",
+ "coil:0",
+ "coil:1-0",
+ "coil:1-1",
+ "coil:1-2"
+ ));
+ modbusCollect.preCheck(metrics);
+ modbusCollect.collect(builder, 1L, "app", metrics);
+ });
+
+
+ // with slaveId2
+ // with holding-register
+ assertDoesNotThrow(() -> {
+ ModbusProtocol modbus = ModbusProtocol.builder().build();
+ modbus.setHost("127.0.0.1");
+ modbus.setPort("502");
+ modbus.setDriverName("modbus-tcp");
+ modbus.setAddressSyntax("holding-register");
+ modbus.setRegisterAddresses(List.of("1", "2[3]"));
+ modbus.setSlaveId("2");
+ modbus.setTimeout("500");
+
+ metrics.setModbus(modbus);
+ metrics.setAliasFields(List.of(
+ "responseTime",
+ "holding-register:0",
+ "holding-register:1-0",
+ "holding-register:1-1",
+ "holding-register:1-2"
+ ));
+ modbusCollect.preCheck(metrics);
+ modbusCollect.collect(builder, 1L, "app", metrics);
+ });
+
+ }
+
+}
diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/plc/PlcCollectTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/plc/PlcCollectTest.java
new file mode 100644
index 00000000000..f38e6b9f566
--- /dev/null
+++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/plc/PlcCollectTest.java
@@ -0,0 +1,253 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package org.apache.hertzbeat.collector.collect.plc;
+
+
+import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
+import org.apache.hertzbeat.common.entity.job.Metrics;
+import org.apache.hertzbeat.common.entity.job.protocol.PlcProtocol;
+import org.apache.hertzbeat.common.entity.message.CollectRep;
+import org.apache.plc4x.java.api.PlcConnection;
+import org.apache.plc4x.java.api.messages.PlcReadRequest;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Test case for {@link AbstractPlcCollectImpl}
+ */
+public class PlcCollectTest {
+ private AbstractPlcCollectImpl plcCollect;
+ private Metrics metrics;
+ private CollectRep.MetricsData.Builder builder;
+
+ @BeforeEach
+ public void setup() {
+ plcCollect = new AbstractPlcCollectImpl() {
+ @Override
+ public String supportProtocol() {
+ return DispatchConstants.PROTOCOL_PLC;
+ }
+
+ @Override
+ protected String getConnectionString(Metrics metrics) {
+ PlcProtocol plcProtocol = metrics.getPlc();
+ return "modbus-tcp:tcp://" + plcProtocol.getHost() + ":" + plcProtocol.getPort() + "?unit-identifier=" + plcProtocol.getSlaveId();
+ }
+
+ @Override
+ protected PlcReadRequest buildRequest(Metrics metrics, PlcConnection connection) {
+ PlcProtocol modbus = metrics.getPlc();
+ List registerAddressList = modbus.getRegisterAddresses();
+ // Create a new read request:
+ PlcReadRequest.Builder requestBuilder = connection.readRequestBuilder();
+ for (int i = 0; i < registerAddressList.size(); i++) {
+ String s1 = modbus.getAddressSyntax() + ":" + registerAddressList.get(i);
+ requestBuilder.addTagAddress(metrics.getPlc().getAddressSyntax() + ":" + i, s1);
+ }
+ return requestBuilder.build();
+ }
+ };
+ PlcProtocol plc = PlcProtocol.builder().build();
+ metrics = Metrics.builder()
+ .plc(plc)
+ .build();
+ builder = CollectRep.MetricsData.newBuilder();
+ }
+
+ @Test
+ void preCheck() {
+ // host is empty
+ assertThrows(IllegalArgumentException.class, () -> {
+ plcCollect.preCheck(metrics);
+ });
+
+ // port is empty default add 502
+ assertThrows(IllegalArgumentException.class, () -> {
+ PlcProtocol plc = PlcProtocol.builder().build();
+ plc.setHost("127.0.0.1");
+ metrics.setPlc(plc);
+ plcCollect.preCheck(metrics);
+ });
+
+ // driverName version is empty
+ assertThrows(IllegalArgumentException.class, () -> {
+ PlcProtocol plc = PlcProtocol.builder().build();
+ plc.setHost("127.0.0.1");
+ plc.setPort("502");
+ metrics.setPlc(plc);
+ plcCollect.preCheck(metrics);
+ });
+
+ // addressSyntax is empty
+ assertThrows(IllegalArgumentException.class, () -> {
+ PlcProtocol plc = PlcProtocol.builder().build();
+ plc.setHost("127.0.0.1");
+ plc.setPort("502");
+ plc.setDriverName("modbus-tcp");
+ metrics.setPlc(plc);
+ plcCollect.preCheck(metrics);
+ });
+
+ // registerAddresses version is empty
+ assertThrows(IllegalArgumentException.class, () -> {
+ PlcProtocol plc = PlcProtocol.builder().build();
+ plc.setHost("127.0.0.1");
+ plc.setPort("502");
+ plc.setDriverName("modbus-tcp");
+ plc.setAddressSyntax("holding-register");
+ metrics.setPlc(plc);
+ plcCollect.preCheck(metrics);
+ });
+
+ // aliasFields is empty
+ assertThrows(IllegalArgumentException.class, () -> {
+ PlcProtocol plc = PlcProtocol.builder().build();
+ plc.setHost("127.0.0.1");
+ plc.setPort("502");
+ plc.setDriverName("modbus-tcp");
+ plc.setAddressSyntax("holding-register");
+ plc.setRegisterAddresses(List.of("1"));
+ metrics.setPlc(plc);
+ plcCollect.preCheck(metrics);
+ });
+
+ // the number of aliasFields is not equal to registerAddresses(responseTime doesn't count)
+ assertThrows(IllegalArgumentException.class, () -> {
+ PlcProtocol plc = PlcProtocol.builder().build();
+ plc.setHost("127.0.0.1");
+ plc.setPort("502");
+ plc.setDriverName("modbus-tcp");
+ plc.setAddressSyntax("holding-register");
+ plc.setRegisterAddresses(List.of("1", "2"));
+ metrics.setAliasFields(List.of(
+ "responseTime",
+ "holding-register:0"
+ ));
+ metrics.setPlc(plc);
+ plcCollect.preCheck(metrics);
+ });
+
+ // everything is ok
+ assertDoesNotThrow(() -> {
+ PlcProtocol plc = PlcProtocol.builder().build();
+ plc.setHost("127.0.0.1");
+ plc.setPort("502");
+ plc.setDriverName("modbus-tcp");
+ plc.setAddressSyntax("holding-register");
+ plc.setRegisterAddresses(List.of("1", "2", "3[2]"));
+ plc.setSlaveId("2");
+ plc.setTimeout("500");
+ metrics.setPlc(plc);
+ metrics.setAliasFields(List.of(
+ "responseTime",
+ "holding-register:0",
+ "holding-register:1",
+ "holding-register:2-0",
+ "holding-register:2-1"
+ ));
+ plcCollect.preCheck(metrics);
+ });
+ }
+
+ @Test
+ void supportProtocol() {
+ Assertions.assertEquals(DispatchConstants.PROTOCOL_PLC, plcCollect.supportProtocol());
+ }
+
+ @Test
+ void collect() {
+ // with holding-register
+ assertDoesNotThrow(() -> {
+ PlcProtocol plc = PlcProtocol.builder().build();
+ plc.setHost("127.0.0.1");
+ plc.setPort("502");
+ plc.setDriverName("modbus-tcp");
+ plc.setAddressSyntax("holding-register");
+ plc.setRegisterAddresses(List.of("1", "2[3]"));
+ plc.setSlaveId("1");
+ plc.setTimeout("500");
+
+ metrics.setPlc(plc);
+ metrics.setAliasFields(List.of(
+ "responseTime",
+ "holding-register:0",
+ "holding-register:1-0",
+ "holding-register:1-1",
+ "holding-register:1-2"
+ ));
+ plcCollect.preCheck(metrics);
+ plcCollect.collect(builder, 1L, "app", metrics);
+ });
+
+ // with coil
+ assertDoesNotThrow(() -> {
+ PlcProtocol plc = PlcProtocol.builder().build();
+ plc.setHost("127.0.0.1");
+ plc.setPort("502");
+ plc.setDriverName("modbus-tcp");
+ plc.setAddressSyntax("coil");
+ plc.setRegisterAddresses(List.of("1", "2[3]"));
+ plc.setSlaveId("1");
+ plc.setTimeout("500");
+
+ metrics.setPlc(plc);
+ metrics.setAliasFields(List.of(
+ "responseTime",
+ "coil:0",
+ "coil:1-0",
+ "coil:1-1",
+ "coil:1-2"
+ ));
+ plcCollect.preCheck(metrics);
+ plcCollect.collect(builder, 1L, "app", metrics);
+ });
+
+
+ // with slaveId2
+ // with holding-register
+ assertDoesNotThrow(() -> {
+ PlcProtocol plc = PlcProtocol.builder().build();
+ plc.setHost("127.0.0.1");
+ plc.setPort("502");
+ plc.setDriverName("modbus-tcp");
+ plc.setAddressSyntax("holding-register");
+ plc.setRegisterAddresses(List.of("1", "2[3]"));
+ plc.setSlaveId("2");
+ plc.setTimeout("500");
+
+ metrics.setPlc(plc);
+ metrics.setAliasFields(List.of(
+ "responseTime",
+ "holding-register:0",
+ "holding-register:1-0",
+ "holding-register:1-1",
+ "holding-register:1-2"
+ ));
+ plcCollect.preCheck(metrics);
+ plcCollect.collect(builder, 1L, "app", metrics);
+ });
+
+ }
+
+}
diff --git a/hertzbeat-collector/hertzbeat-collector-collector/src/main/resources/META-INF/services/org.apache.hertzbeat.collector.collect.AbstractCollect b/hertzbeat-collector/hertzbeat-collector-collector/src/main/resources/META-INF/services/org.apache.hertzbeat.collector.collect.AbstractCollect
index b6551797e2a..0cbd4f01ea8 100644
--- a/hertzbeat-collector/hertzbeat-collector-collector/src/main/resources/META-INF/services/org.apache.hertzbeat.collector.collect.AbstractCollect
+++ b/hertzbeat-collector/hertzbeat-collector-collector/src/main/resources/META-INF/services/org.apache.hertzbeat.collector.collect.AbstractCollect
@@ -29,3 +29,4 @@ org.apache.hertzbeat.collector.collect.mqtt.MqttCollectImpl
org.apache.hertzbeat.collector.collect.ipmi2.IpmiCollectImpl
org.apache.hertzbeat.collector.collect.kafka.KafkaCollectImpl
org.apache.hertzbeat.collector.collect.sd.HttpSdCollectImpl
+org.apache.hertzbeat.collector.collect.modbus.ModbusCollectImpl
\ No newline at end of file
diff --git a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/DispatchConstants.java b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/DispatchConstants.java
index e6d00dfa179..569950cb218 100644
--- a/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/DispatchConstants.java
+++ b/hertzbeat-collector/hertzbeat-collector-common/src/main/java/org/apache/hertzbeat/collector/dispatch/DispatchConstants.java
@@ -217,4 +217,14 @@ public interface DispatchConstants {
* protocol kafka
*/
String PROTOCOL_KAFKA = "kclient";
+
+ /**
+ * protocol plc
+ */
+ String PROTOCOL_PLC = "plc";
+
+ /**
+ * protocol modbus
+ */
+ String PROTOCOL_MODBUS = "modbus";
}
diff --git a/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/job/Metrics.java b/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/job/Metrics.java
index 261f3bfc48c..6a2838577e8 100644
--- a/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/job/Metrics.java
+++ b/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/job/Metrics.java
@@ -32,6 +32,8 @@
import org.apache.hertzbeat.common.entity.job.protocol.DnsProtocol;
import org.apache.hertzbeat.common.entity.job.protocol.FtpProtocol;
import org.apache.hertzbeat.common.entity.job.protocol.HttpProtocol;
+import org.apache.hertzbeat.common.entity.job.protocol.ModbusProtocol;
+import org.apache.hertzbeat.common.entity.job.protocol.PlcProtocol;
import org.apache.hertzbeat.common.entity.job.protocol.RegistryProtocol;
import org.apache.hertzbeat.common.entity.job.protocol.IcmpProtocol;
import org.apache.hertzbeat.common.entity.job.protocol.ImapProtocol;
@@ -255,7 +257,14 @@ public class Metrics {
* Collect sd data protocol
*/
private ServiceDiscoveryProtocol sdProtocol;
-
+ /**
+ * Monitoring configuration information using the public plc protocol
+ */
+ private PlcProtocol plc;
+ /**
+ * Monitoring configuration information using the public modBus protocol
+ */
+ private ModbusProtocol modbus;
/**
* collector use - Temporarily store subTask metrics response data
*/
diff --git a/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/ModbusProtocol.java b/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/ModbusProtocol.java
new file mode 100644
index 00000000000..dff07ad4436
--- /dev/null
+++ b/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/ModbusProtocol.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package org.apache.hertzbeat.common.entity.job.protocol;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+
+/**
+ * Modbus Protocol
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class ModbusProtocol {
+
+ /**
+ * IP ADDRESS OR DOMAIN NAME OF THE PEER HOST
+ */
+ private String host;
+ /**
+ * Port number
+ */
+ private String port;
+
+ private String driverName;
+
+ private String addressSyntax;
+
+ private String slaveId;
+
+ private String timeout;
+
+ private List registerAddresses;
+}
diff --git a/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/PlcProtocol.java b/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/PlcProtocol.java
new file mode 100644
index 00000000000..bbc95872909
--- /dev/null
+++ b/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/job/protocol/PlcProtocol.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package org.apache.hertzbeat.common.entity.job.protocol;
+
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+/**
+ * Plc Protocol
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class PlcProtocol {
+ /**
+ * IP ADDRESS OR DOMAIN NAME OF THE PEER HOST
+ */
+ private String host;
+ /**
+ * Port number
+ */
+ private String port;
+
+ private String driverName;
+
+ private String addressSyntax;
+
+ private String slaveId;
+
+ private String timeout;
+
+ private List registerAddresses;
+
+}
diff --git a/hertzbeat-manager/src/main/resources/define/app-modbus.yml b/hertzbeat-manager/src/main/resources/define/app-modbus.yml
new file mode 100644
index 00000000000..4da9df377be
--- /dev/null
+++ b/hertzbeat-manager/src/main/resources/define/app-modbus.yml
@@ -0,0 +1,236 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+# The monitoring type category:service-application service monitoring db-database monitoring mid-middleware custom-custom monitoring os-operating system monitoring
+category: service
+# The monitoring type eg: linux windows tomcat mysql aws...
+app: modbus
+# The app api i18n name
+name:
+ zh-CN: ModBus服务器
+ en-US: ModBus Server
+# The description and help of this monitoring type
+help:
+ zh-CN: HertzBeat对支持Modbus协议的服务进行(保持寄存器和线圈)相关指标进行采集
+ en-US: HertzBeat collects metrics related to maintaining registers and coils for services that support Modbus protocol
+ zh-TW: HertzBeat對支持Modbus協定的服務進行(保持寄存器和線圈)相關名額進行採集
+# Input params define for monitoring(render web ui by the definition)
+params:
+ # field-param field key
+ - field: host
+ # name-param field display i18n name
+ name:
+ zh-CN: ModBus服务Host
+ en-US: ModBus Server Host
+ # type-param field type(most mapping the html input type)
+ type: host
+ # required-true or false
+ required: true
+ # field-param field key
+ - field: port
+ # name-param field display i18n name
+ name:
+ zh-CN: 端口
+ en-US: Port
+ # type-param field type(most mapping the html input type)
+ type: number
+ # when type is number, range is required
+ range: '[0,65535]'
+ # required-true or false
+ required: true
+ # default value
+ defaultValue: 502
+ - field: slaveId
+ # name-param field display i18n name
+ name:
+ zh-CN: slaveId
+ en-US: slaveId
+ # type-param field type(most mapping the html input type)
+ type: text
+ # required-true or false
+ required: false
+ defaultValue: 1
+ - field: timeout
+ name:
+ zh-CN: 请求超时时间(ms)
+ en-US: Request Timeout(ms)
+ type: number
+ # when type is number, range is required
+ range: '[400,200000]'
+ required: false
+ defaultValue: 6000
+ hide: true
+ - field: holdingRegisterAddresses
+ # name-param field display i18n name
+ name:
+ zh-CN: 保持寄存器地址
+ en-US: Holding Registers address
+ # type-param field type(most mapping the html input type)
+ type: array
+ # param field input placeholder
+ placeholder: 'Input RegisterAddress'
+ # required-true or false
+ required: true
+ # hide param-true or false
+ # hide: true
+ - field: coilRegisterAddresses
+ # name-param field display i18n name
+ name:
+ zh-CN: 线圈寄存器地址
+ en-US: Coil Register address
+ # type-param field type(most mapping the html input type)
+ type: array
+ # param field input placeholder
+ placeholder: 'Input RegisterAddress'
+ # required-true or false
+ required: true
+ # hide param-true or false
+# hide: true
+
+# collect metrics config list
+metrics:
+ # metrics - summary
+ - name: holding-register
+ i18n:
+ zh-CN: 保持寄存器 统计信息
+ en-US: holding-register stats
+ # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel
+ # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue
+ priority: 0
+ # collect metrics content
+ fields:
+ - field: responseTime
+ type: 0
+ unit: ms
+ i18n:
+ zh-CN: 响应时间
+ en-US: Response Time
+ # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field
+ - field: address-0
+ type: 0
+ i18n:
+ zh-CN: address-0
+ en-US: address-0
+ # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field
+ - field: address-1-0
+ type: 0
+ i18n:
+ zh-CN: address-1-0
+ en-US: address-1-0
+ - field: address-1-1
+ type: 0
+ i18n:
+ zh-CN: address-1-1
+ en-US: address-1-1
+ - field: address-1-2
+ type: 0
+ i18n:
+ zh-CN: address-1-2
+ en-US: address-1-2
+ # 指标别名列表,按照寄存器地址来进行命名的
+ # 例如 地址为下标为0 ,值为m holding-register:0
+ # 地址为下标为1,值为m[2] holding-register:1-0 holding-register:1-1
+ aliasFields:
+ - responseTime
+ - holding-register:0
+ - holding-register:1-0
+ - holding-register:1-1
+ - holding-register:1-2
+ # mapping and conversion expressions, use thesand aliasField above to calculate metrics value# (可选)指标映射转换计算表达式,与上面的别名一起作用,计算出最终需要的指标值# eg: cores=core1+core2, usage=usage, waitTimeallTime-runningTime
+ calculates:
+ - responseTime=responseTime
+ - address-0=holding-register:0
+ - address-1-0=holding-register:1-0
+ - address-1-1=holding-register:1-1
+ - address-1-2=holding-register:1-2
+ protocol: modbus
+ # the config content when protocol is http
+ modbus:
+ # host
+ host: ^_^host^_^
+ # port
+ port: ^_^port^_^
+ driverName: modbus-tcp
+ addressSyntax: holding-register
+ slaveId: ^_^slaveId^_^
+ timeout: ^_^timeout^_^
+ registerAddresses: [ ^_^holdingRegisterAddresses^_^ ]
+
+ # metrics - summary
+ - name: coil
+ i18n:
+ zh-CN: 线圈 统计信息
+ en-US: coil stats
+ # metrics scheduling priority(0->127)->(high->low), metrics with the same priority will be scheduled in parallel
+ # priority 0's metrics is availability metrics, it will be scheduled first, only availability metrics collect success will the scheduling continue
+ priority: 1
+ # collect metrics content
+ fields:
+ - field: responseTime
+ type: 0
+ unit: ms
+ i18n:
+ zh-CN: 响应时间
+ en-US: Response Time
+ # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field
+ - field: address-0
+ type: 0
+ i18n:
+ zh-CN: address-0
+ en-US: address-0
+ # field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field
+ - field: address-1-0
+ type: 0
+ i18n:
+ zh-CN: address-1-0
+ en-US: address-1-0
+ - field: address-1-1
+ type: 0
+ i18n:
+ zh-CN: address-1-1
+ en-US: address-1-1
+ - field: address-1-2
+ type: 0
+ i18n:
+ zh-CN: address-1-2
+ en-US: address-1-2
+ # 指标别名列表,按照寄存器地址来进行命名的
+ # 例如 地址为下标为0 ,值为m coil:0
+ # 地址为下标为1,值为m[2] coil:1-0 coil:1-1
+ aliasFields:
+ - responseTime
+ - coil:0
+ - coil:1-0
+ - coil:1-1
+ - coil:1-2
+ # mapping and conversion expressions, use thesand aliasField above to calculate metrics value# (可选)指标映射转换计算表达式,与上面的别名一起作用,计算出最终需要的指标值# eg: cores=core1+core2, usage=usage, waitTimeallTime-runningTime
+ calculates:
+ - responseTime=responseTime
+ - address-0=coil:0
+ - address-1-0=coil:1-0
+ - address-1-1=coil:1-1
+ - address-1-2=coil:1-2
+ protocol: modbus
+ # the config content when protocol is http
+ modbus:
+ # host
+ host: ^_^host^_^
+ # port
+ port: ^_^port^_^
+ driverName: modbus-tcp
+ addressSyntax: coil
+ slaveId: ^_^slaveId^_^
+ timeout: ^_^timeout^_^
+ registerAddresses: [ ^_^coilRegisterAddresses^_^ ]
\ No newline at end of file
diff --git a/home/docs/help/modbus.md b/home/docs/help/modbus.md
new file mode 100644
index 00000000000..a26462cb410
--- /dev/null
+++ b/home/docs/help/modbus.md
@@ -0,0 +1,80 @@
+---
+id: modbus
+title: Monitoring Modbus
+sidebar_label: Modbus Monitor
+keywords: [ open source monitoring tool, Modbus monitoring ]
+---
+
+> The response of Modbus service and other related indicators are monitored.
+
+### Configuration Parameters
+
+| Parameter Name | Parameter Help Description |
+|--------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
+| Host of Modbus Service | The IPv4, IPv6, or domain name of the Modbus device to be monitored. Note ⚠️ do not include the protocol header (e.g., https://, http://). |
+| Task Name | A name that identifies this monitoring task; the name must be unique. |
+| Port | The port used for Modbus network communication. |
+| Slave ID (slaveId) | The ID of the slave device in the Modbus network. |
+| Holding Register Address | Used for categorizing and managing monitored resources. |
+| Coil Register Address | Additional notes and descriptions for this monitoring task; users can add remarks here. |
+| Timeout | The allowed time for collecting a response. |
+
+### Collected Metrics
+
+#### Metric Set: holding-register
+
+1. The number of parameters must match the total number of coil register addresses specified in the parameters.
+2. Alias format for parameters: holding-register:m or holding-register:m-n
+
+Parameter example:
+
+Coil register addresses:
+
+```text
+1,2[3]
+```
+
+Parameter alias names:
+
+```yaml
+aliasFields:
+ - responseTime
+ - holding-register:0
+ - holding-register:1-0
+ - holding-register:1-1
+ - holding-register:1-2
+```
+
+| Metric Name | Metric Unit | Metric Help Description |
+|----------------------------|--------------|-----------------------------------------------------------------|
+| Response Time | Milliseconds | The time required by the Modbus server to respond to a request. |
+| Holding Register Parameter | | Setpoint for analog output |
+
+#### Metric Set: coil
+
+1. The number of parameters must match the total number of coil register addresses specified in the parameters.
+2. Alias format for parameters: coil:m or coil:m-n
+
+Parameter example:
+
+Coil register addresses:
+
+```text
+1,2[3]
+```
+
+Parameter alias names:
+
+```yaml
+aliasFields:
+ - responseTime
+ - coil:0
+ - coil:1-0
+ - coil:1-1
+ - coil:1-2
+```
+
+| Metric Name | Metric Unit | Metric Help Description |
+|---------------|--------------|-----------------------------------------------------------------|
+| Response Time | Milliseconds | The time required by the Modbus server to respond to a request. |
+| Coil Status | | Coil status (0 or 1) |
diff --git a/home/i18n/zh-cn/docusaurus-plugin-content-docs/current/help/modbus.md b/home/i18n/zh-cn/docusaurus-plugin-content-docs/current/help/modbus.md
new file mode 100644
index 00000000000..d17a81bc620
--- /dev/null
+++ b/home/i18n/zh-cn/docusaurus-plugin-content-docs/current/help/modbus.md
@@ -0,0 +1,80 @@
+---
+id: modbus
+title: Monitoring Modbus
+sidebar_label: Modbus Monitor
+keywords: [ open source monitoring tool, Modbus监控 ]
+---
+
+> Modbus 服务的响应等相关指标进行监测。
+
+### 配置参数
+
+| 参数名称 | 参数帮助描述 |
+|---------------|-----------------------------------------------------------|
+| Modbus服务的Host | 被监控的Modbus的IPV4,IPV6或域名。注意⚠️不带协议头(eg: https://, http://)。 |
+| 任务名称 | 标识此监控的名称,名称需要保证唯一性。 |
+| 端口 | Modbus网络的端口。 |
+| slaveId | Modbus网络中从机设备ID。 |
+| 保持寄存器地址 | 用于对监控资源进行分类管理。 |
+| 线圈寄存器地址 | 更多标识和描述此监控的备注信息,用户可以在这里备注信息。 |
+| 超时 | 允许收集响应时间 |
+
+### 采集指标
+
+#### 指标集合:holding-register
+
+1. 参数数量需要与参数中线圈寄存器地址的总数量一样
+2. 参数别名格式: holding-register:m 或 holding-register:m-n
+
+参数示例:
+
+线圈寄存器地址:
+
+```text
+1,2[3]
+```
+
+参数别名名称:
+
+```yaml
+aliasFields:
+ - responseTime
+ - holding-register:0
+ - holding-register:1-0
+ - holding-register:1-1
+ - holding-register:1-2
+```
+
+| 指标名称 | 指标单位 | 指标帮助描述 |
+|---------|------|---------------------|
+| 响应时间 | 毫秒 | Modbus服务器响应请求所需的时间。 |
+| 保持寄存器参数 | | 模拟量输出设定值 |
+
+#### 指标集合:coil
+
+1. 参数数量需要与参数中线圈寄存器地址的总数量一样
+2. 参数别名格式: coil:m 或 coil:m-n
+
+参数示例:
+
+线圈寄存器地址:
+
+```text
+1,2[3]
+```
+
+参数别名名称:
+
+```yaml
+aliasFields:
+ - responseTime
+ - coil:0
+ - coil:1-0
+ - coil:1-1
+ - coil:1-2
+```
+
+| 指标名称 | 指标单位 | 指标帮助描述 |
+|------|------|---------------------|
+| 响应时间 | 毫秒 | Modbus服务器响应请求所需的时间。 |
+| 线圈状态 | | 线圈状态 (0或1) |
diff --git a/home/sidebars.json b/home/sidebars.json
index 5e701f65569..d6e0c7b7b92 100755
--- a/home/sidebars.json
+++ b/home/sidebars.json
@@ -92,7 +92,8 @@
"help/dns",
"help/ftp",
"help/websocket",
- "help/mqtt"
+ "help/mqtt",
+ "help/modbus"
]
},
{
diff --git a/material/licenses/LICENSE b/material/licenses/LICENSE
index 9f9a3a4bea0..19d9fcaa84b 100644
--- a/material/licenses/LICENSE
+++ b/material/licenses/LICENSE
@@ -357,6 +357,8 @@ The text of each license is the standard Apache 2.0 license.
https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-el/10.1.19 Apache-2.0
https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-websocket/10.1.19 Apache-2.0
https://mvnrepository.com/artifact/org.apache.xmlbeans/xmlbeans/3.1.0 Apache-2.0
+ https://mvnrepository.com/artifact/org.apache.plc4x/plc4j-api/0.12.0 Apache-2.0
+ https://mvnrepository.com/artifact/org.apache.plc4x/plc4j-driver-modbus/0.12.0 Apache-2.0
https://mvnrepository.com/artifact/org.attoparser/attoparser/2.0.7.RELEASE Apache-2.0
https://mvnrepository.com/artifact/org.freemarker/freemarker/2.3.32 Apache-2.0
https://mvnrepository.com/artifact/org.flywaydb/flyway-core/10.11.1
diff --git a/material/licenses/backend/LICENSE b/material/licenses/backend/LICENSE
index 958cd9b343f..31c242b64c6 100644
--- a/material/licenses/backend/LICENSE
+++ b/material/licenses/backend/LICENSE
@@ -357,6 +357,8 @@ The text of each license is the standard Apache 2.0 license.
https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-el/10.1.19 Apache-2.0
https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-websocket/10.1.19 Apache-2.0
https://mvnrepository.com/artifact/org.apache.xmlbeans/xmlbeans/3.1.0 Apache-2.0
+ https://mvnrepository.com/artifact/org.apache.plc4x/plc4j-api/0.12.0 Apache-2.0
+ https://mvnrepository.com/artifact/org.apache.plc4x/plc4j-driver-modbus/0.12.0 Apache-2.0
https://mvnrepository.com/artifact/org.attoparser/attoparser/2.0.7.RELEASE Apache-2.0
https://mvnrepository.com/artifact/org.freemarker/freemarker/2.3.32 Apache-2.0
https://mvnrepository.com/artifact/org.flywaydb/flyway-core/10.11.1
diff --git a/material/licenses/collector/LICENSE b/material/licenses/collector/LICENSE
index 2620a42bd70..60853a9f8bc 100644
--- a/material/licenses/collector/LICENSE
+++ b/material/licenses/collector/LICENSE
@@ -288,6 +288,8 @@ The text of each license is the standard Apache 2.0 license.
https://mvnrepository.com/artifact/org.apache.sshd/sshd-core/2.8.0 Apache-2.0
https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-el/10.1.19 Apache-2.0
https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-websocket/10.1.19 Apache-2.0
+ https://mvnrepository.com/artifact/org.apache.plc4x/plc4j-api/0.12.0 Apache-2.0
+ https://mvnrepository.com/artifact/org.apache.plc4x/plc4j-driver-modbus/0.12.0 Apache-2.0
https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator/8.0.1.Final Apache-2.0
https://mvnrepository.com/artifact/org.lz4/lz4-java/1.8.0 Apache-2.0
https://mvnrepository.com/artifact/org.mongodb/mongodb-driver-core/4.6.1 Apache-2.0