pointConfig, IotConnect connect, IotConfig config) {
+ return new RValue(config, connect, readValue(getConnector(connect.getId().toString(), driverConfig), pointConfig));
+ }
+
+ /**
+ * 获取 Modbus Master 连接器
+ *
+ * 该方法用于根据设备ID和驱动配置获取或创建 Modbus Master 连接器。
+ * 如果连接器已存在, 则直接返回;否则, 根据配置创建新的连接器并初始化。
+ * 初始化失败时, 会移除连接器并抛出异常。
+ *
+ * @param connectId 连接ID(一个设备对应多个连接)
+ * @param driverConfig 驱动配置, 包含连接 Modbus 设备所需的主机地址和端口号
+ * @return ModbusMaster 返回与设备关联的 Modbus Master 连接器
+ * @throws CommonException 如果连接器初始化失败, 抛出此异常
+ */
+ private ModbusMaster getConnector(String connectId, Map driverConfig) {
+ log.debug("Modbus Tcp Connection Info: {}", driverConfig);
+ ModbusMaster modbusMaster = connectMap.get(connectId);
+ if (Objects.isNull(modbusMaster)) {
+ IpParameters params = new IpParameters();
+ params.setHost(driverConfig.get("host").getValueByClass(String.class));
+ params.setPort(driverConfig.get("port").getValueByClass(Integer.class));
+ modbusMaster = modbusFactory.createTcpMaster(params, true);
+ try {
+ modbusMaster.init();
+ connectMap.put(connectId, modbusMaster);
+ } catch (ModbusInitException e) {
+ connectMap.entrySet().removeIf(next -> next.getKey().equals(connectId));
+ log.error("Connect modbus master error: {}", e.getMessage(), e);
+ throw new CommonException(e.getMessage());
+ }
+ }
+ return modbusMaster;
+ }
+
+ /**
+ * 读取 Modbus 设备点位值
+ *
+ * 根据点位配置中的功能码(functionCode)和偏移量(offset), 从 Modbus 设备中读取相应类型的值。
+ * 支持的功能码包括:
+ * - 1: 读取线圈状态(Coil Status)
+ * - 2: 读取输入状态(Input Status)
+ * - 3: 读取保持寄存器(Holding Register)
+ * - 4: 读取输入寄存器(Input Register)
+ *
+ * @param modbusMaster ModbusMaster 连接器, 用于与设备通信
+ * @param pointConfig 点位配置, 包含从站ID(slaveId), 功能码(functionCode), 偏移量(offset)等信息
+ * @param type 点位值类型, 用于确定寄存器中数据的解析方式
+ * @return String 返回读取到的点位值, 以字符串形式表示。如果功能码不支持, 则返回 "0"。
+ */
+ private String readValue(ModbusMaster modbusMaster, Map pointConfig) {
+ String type = pointConfig.get("data_type").getValueByClass(String.class);
+ int slaveId = pointConfig.get("slaveId").getValueByClass(Integer.class);
+ int offset = pointConfig.get("offset").getValueByClass(Integer.class);
+ int functionCode = getFunctionCode(offset);
+
+ switch (functionCode) {
+ case 1:
+ BaseLocator coilLocator = BaseLocator.coilStatus(slaveId, offset);
+ Boolean coilValue = getMasterValue(modbusMaster, coilLocator);
+ return String.valueOf(coilValue);
+ case 2:
+ BaseLocator inputLocator = BaseLocator.inputStatus(slaveId, offset);
+ Boolean inputStatusValue = getMasterValue(modbusMaster, inputLocator);
+ return String.valueOf(inputStatusValue);
+ case 3:
+ BaseLocator holdingLocator = BaseLocator.holdingRegister(slaveId, offset, getValueType(type));
+ Number holdingValue = getMasterValue(modbusMaster, holdingLocator);
+ return String.valueOf(holdingValue);
+ case 4:
+ BaseLocator inputRegister = BaseLocator.inputRegister(slaveId, offset, getValueType(type));
+ Number inputRegisterValue = getMasterValue(modbusMaster, inputRegister);
+ return String.valueOf(inputRegisterValue);
+ default:
+ return "0";
+ }
+ }
+
+ /**
+ * 从 ModbusMaster 连接器中读取指定点位的数据
+ *
+ * 该方法通过给定的 {@link BaseLocator} 从 ModbusMaster 连接器中读取数据。
+ * 如果读取过程中发生 {@link ModbusTransportException} 或 {@link ErrorResponseException} 异常,
+ * 将记录错误日志并抛出 {@link CommonException} 异常。
+ *
+ * @param modbusMaster ModbusMaster 连接器, 用于与设备通信
+ * @param locator 点位定位器, 包含从站ID, 功能码, 偏移量等信息
+ * @param 返回值类型, 根据点位的数据类型确定
+ * @return T 返回读取到的点位数据
+ * @throws CommonException 如果读取过程中发生异常, 抛出此异常
+ */
+ private T getMasterValue(ModbusMaster modbusMaster, BaseLocator locator) {
+ try {
+ return modbusMaster.getValue(locator);
+ } catch (ModbusTransportException | ErrorResponseException e) {
+ log.error("Read modbus master value error: {}", e.getMessage(), e);
+ throw new CommonException(e.getMessage());
+ }
+ }
+
+ /**
+ * 获取 Modbus 数据类型
+ *
+ * 根据点位值类型(type)返回对应的 Modbus 数据类型。
+ * - 其他类型: 返回 2 字节有符号整数({@link DataType#TWO_BYTE_INT_SIGNED})
+ *
+ * 提示: 该方法可根据实际项目需求进行扩展, 例如支持字节交换, 大端/小端模式等。
+ *
+ * @param type 点位值类型, 用于确定 Modbus 数据类型
+ * @return int 返回对应的 Modbus 数据类型
+ * @throws CommonException 如果点位值类型不支持, 抛出此异常
+ */
+ private int getValueType(String type) {
+
+ switch (type.toLowerCase()) {
+ case "int32":
+ return DataType.FOUR_BYTE_INT_SIGNED;
+ case "uint32":
+ return DataType.FOUR_BYTE_INT_UNSIGNED;
+ case "double":
+ return DataType.EIGHT_BYTE_FLOAT;
+ case "float32":
+ return DataType.FOUR_BYTE_FLOAT;
+ default:
+ return DataType.TWO_BYTE_INT_SIGNED;
+ }
+ }
+
+ /**
+ * 根据Modbus地址偏移量获取对应的功能码
+ * @param offset 地址偏移量(1-9999/10001-19999/30001-39999/40001-49999)
+ * @return 对应的功能码(1-4)
+ * @throws CommonException 当偏移量不在合法范围时抛出异常
+ */
+
+ public static int getFunctionCode(int offset) {
+ int functionCode;
+
+ if (offset >= 1 && offset <= 9999) {
+ functionCode = 1;
+ } else if (offset >= 10001 && offset <= 19999) {
+ functionCode = 2;
+ } else if (offset >= 30001 && offset <= 39999) {
+ functionCode = 3;
+ } else if (offset >= 40001 && offset <= 49999) {
+ functionCode = 4;
+ } else {
+ throw new CommonException("无效的偏移量:" + offset);
+ }
+
+ return functionCode;
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/BasicProcessImage.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/BasicProcessImage.java
new file mode 100644
index 0000000..72d834a
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/BasicProcessImage.java
@@ -0,0 +1,569 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.ModbusUtils;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.RangeAndOffset;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.RegisterRange;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.IllegalDataAddressException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusIdException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.locator.BaseLocator;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.locator.NumericLocator;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.locator.StringLocator;
+
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ *
BasicProcessImage class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class BasicProcessImage implements ProcessImage {
+ private final int slaveId;
+ private final Map coils = new HashMap<>();
+ private final Map inputs = new HashMap<>();
+ private final Map holdingRegisters = new HashMap<>();
+ private final Map inputRegisters = new HashMap<>();
+ private final List writeListeners = new ArrayList<>();
+ private boolean allowInvalidAddress = false;
+ private short invalidAddressValue = 0;
+ private byte exceptionStatus;
+
+ /**
+ * Constructor for BasicProcessImage.
+ *
+ * @param slaveId a int.
+ */
+ public BasicProcessImage(int slaveId) {
+ ModbusUtils.validateSlaveId(slaveId, false);
+ this.slaveId = slaveId;
+ }
+
+ @Override
+ public int getSlaveId() {
+ return slaveId;
+ }
+
+ /**
+ * addListener.
+ *
+ * @param l a {@link ProcessImageListener} object.
+ */
+ public synchronized void addListener(ProcessImageListener l) {
+ writeListeners.add(l);
+ }
+
+ /**
+ * removeListener.
+ *
+ * @param l a {@link ProcessImageListener} object.
+ */
+ public synchronized void removeListener(ProcessImageListener l) {
+ writeListeners.remove(l);
+ }
+
+ /**
+ * isAllowInvalidAddress.
+ *
+ * @return a boolean.
+ */
+ public boolean isAllowInvalidAddress() {
+ return allowInvalidAddress;
+ }
+
+ /**
+ * Setter for the field allowInvalidAddress.
+ *
+ * @param allowInvalidAddress a boolean.
+ */
+ public void setAllowInvalidAddress(boolean allowInvalidAddress) {
+ this.allowInvalidAddress = allowInvalidAddress;
+ }
+
+ /**
+ * Getter for the field invalidAddressValue.
+ *
+ * @return a short.
+ */
+ public short getInvalidAddressValue() {
+ return invalidAddressValue;
+ }
+
+ /**
+ * Setter for the field invalidAddressValue.
+ *
+ * @param invalidAddressValue a short.
+ */
+ public void setInvalidAddressValue(short invalidAddressValue) {
+ this.invalidAddressValue = invalidAddressValue;
+ }
+
+ //
+ // /
+ // / Additional convenience methods.
+ // /
+ //
+
+ /**
+ * setBinary.
+ *
+ * @param registerId a int.
+ * @param value a boolean.
+ */
+ public void setBinary(int registerId, boolean value) {
+ RangeAndOffset rao = new RangeAndOffset(registerId);
+ setBinary(rao.getRange(), rao.getOffset(), value);
+ }
+
+ //
+ // Binaries
+
+ /**
+ * setBinary.
+ *
+ * @param range a int.
+ * @param offset a int.
+ * @param value a boolean.
+ */
+ public void setBinary(int range, int offset, boolean value) {
+ if (range == RegisterRange.COIL_STATUS)
+ setCoil(offset, value);
+ else if (range == RegisterRange.INPUT_STATUS)
+ setInput(offset, value);
+ else
+ throw new ModbusIdException("Invalid range to set binary: " + range);
+ }
+
+ /**
+ * setNumeric.
+ *
+ * @param registerId a int.
+ * @param dataType a int.
+ * @param value a {@link Number} object.
+ */
+ public synchronized void setNumeric(int registerId, int dataType, Number value) {
+ RangeAndOffset rao = new RangeAndOffset(registerId);
+ setNumeric(rao.getRange(), rao.getOffset(), dataType, value);
+ }
+
+ //
+ // Numerics
+
+ /**
+ * setNumeric.
+ *
+ * @param range a int.
+ * @param offset a int.
+ * @param dataType a int.
+ * @param value a {@link Number} object.
+ */
+ public synchronized void setNumeric(int range, int offset, int dataType, Number value) {
+ short[] registers = new NumericLocator(slaveId, range, offset, dataType).valueToShorts(value);
+
+ // Write the value.
+ if (range == RegisterRange.HOLDING_REGISTER)
+ setHoldingRegister(offset, registers);
+ else if (range == RegisterRange.INPUT_REGISTER)
+ setInputRegister(offset, registers);
+ else
+ throw new ModbusIdException("Invalid range to set register: " + range);
+ }
+
+ /**
+ * setString.
+ *
+ * @param range a int.
+ * @param offset a int.
+ * @param dataType a int.
+ * @param registerCount a int.
+ * @param s a {@link String} object.
+ */
+ public synchronized void setString(int range, int offset, int dataType, int registerCount, String s) {
+ setString(range, offset, dataType, registerCount, StringLocator.ASCII, s);
+ }
+
+ //
+ // Strings
+
+ /**
+ * setString.
+ *
+ * @param range a int.
+ * @param offset a int.
+ * @param dataType a int.
+ * @param registerCount a int.
+ * @param charset a {@link Charset} object.
+ * @param s a {@link String} object.
+ */
+ public synchronized void setString(int range, int offset, int dataType, int registerCount, Charset charset, String s) {
+ short[] registers = new StringLocator(slaveId, range, offset, dataType, registerCount, charset)
+ .valueToShorts(s);
+
+ // Write the value.
+ if (range == RegisterRange.HOLDING_REGISTER)
+ setHoldingRegister(offset, registers);
+ else if (range == RegisterRange.INPUT_REGISTER)
+ setInputRegister(offset, registers);
+ else
+ throw new ModbusIdException("Invalid range to set register: " + range);
+ }
+
+ /**
+ * setHoldingRegister.
+ *
+ * @param offset a int.
+ * @param registers an array of {@link short} objects.
+ */
+ public synchronized void setHoldingRegister(int offset, short[] registers) {
+ validateOffset(offset);
+ for (int i = 0; i < registers.length; i++)
+ setHoldingRegister(offset + i, registers[i]);
+ }
+
+ /**
+ * setInputRegister.
+ *
+ * @param offset a int.
+ * @param registers an array of {@link short} objects.
+ */
+ public synchronized void setInputRegister(int offset, short[] registers) {
+ validateOffset(offset);
+ for (int i = 0; i < registers.length; i++)
+ setInputRegister(offset + i, registers[i]);
+ }
+
+ /**
+ * setBit.
+ *
+ * @param range a int.
+ * @param offset a int.
+ * @param bit a int.
+ * @param value a boolean.
+ */
+ public synchronized void setBit(int range, int offset, int bit, boolean value) {
+ if (range == RegisterRange.HOLDING_REGISTER)
+ setHoldingRegisterBit(offset, bit, value);
+ else if (range == RegisterRange.INPUT_REGISTER)
+ setInputRegisterBit(offset, bit, value);
+ else
+ throw new ModbusIdException("Invalid range to set register: " + range);
+ }
+
+ //
+ // Bits
+
+ /**
+ * setHoldingRegisterBit.
+ *
+ * @param offset a int.
+ * @param bit a int.
+ * @param value a boolean.
+ */
+ public synchronized void setHoldingRegisterBit(int offset, int bit, boolean value) {
+ validateBit(bit);
+ short s;
+ try {
+ s = getHoldingRegister(offset);
+ } catch (IllegalDataAddressException e) {
+ s = 0;
+ }
+ setHoldingRegister(offset, setBit(s, bit, value));
+ }
+
+ /**
+ * setInputRegisterBit.
+ *
+ * @param offset a int.
+ * @param bit a int.
+ * @param value a boolean.
+ */
+ public synchronized void setInputRegisterBit(int offset, int bit, boolean value) {
+ validateBit(bit);
+ short s;
+ try {
+ s = getInputRegister(offset);
+ } catch (IllegalDataAddressException e) {
+ s = 0;
+ }
+ setInputRegister(offset, setBit(s, bit, value));
+ }
+
+ /**
+ * getBit.
+ *
+ * @param range a int.
+ * @param offset a int.
+ * @param bit a int.
+ * @return a boolean.
+ * @throws IllegalDataAddressException if any.
+ */
+ public boolean getBit(int range, int offset, int bit) throws IllegalDataAddressException {
+ if (range == RegisterRange.HOLDING_REGISTER)
+ return getHoldingRegisterBit(offset, bit);
+ if (range == RegisterRange.INPUT_REGISTER)
+ return getInputRegisterBit(offset, bit);
+ throw new ModbusIdException("Invalid range to get register: " + range);
+ }
+
+ /**
+ * getHoldingRegisterBit.
+ *
+ * @param offset a int.
+ * @param bit a int.
+ * @return a boolean.
+ * @throws IllegalDataAddressException if any.
+ */
+ public boolean getHoldingRegisterBit(int offset, int bit) throws IllegalDataAddressException {
+ validateBit(bit);
+ return getBit(getHoldingRegister(offset), bit);
+ }
+
+ /**
+ * getInputRegisterBit.
+ *
+ * @param offset a int.
+ * @param bit a int.
+ * @return a boolean.
+ * @throws IllegalDataAddressException if any.
+ */
+ public boolean getInputRegisterBit(int offset, int bit) throws IllegalDataAddressException {
+ validateBit(bit);
+ return getBit(getInputRegister(offset), bit);
+ }
+
+ /**
+ * getNumeric.
+ *
+ * @param range a int.
+ * @param offset a int.
+ * @param dataType a int.
+ * @return a {@link Number} object.
+ * @throws IllegalDataAddressException if any.
+ */
+ public Number getNumeric(int range, int offset, int dataType) throws IllegalDataAddressException {
+ return getRegister(new NumericLocator(slaveId, range, offset, dataType));
+ }
+
+ /**
+ * getString.
+ *
+ * @param range a int.
+ * @param offset a int.
+ * @param dataType a int.
+ * @param registerCount a int.
+ * @return a {@link String} object.
+ * @throws IllegalDataAddressException if any.
+ */
+ public String getString(int range, int offset, int dataType, int registerCount) throws IllegalDataAddressException {
+ return getRegister(new StringLocator(slaveId, range, offset, dataType, registerCount, null));
+ }
+
+ /**
+ * getString.
+ *
+ * @param range a int.
+ * @param offset a int.
+ * @param dataType a int.
+ * @param registerCount a int.
+ * @param charset a {@link Charset} object.
+ * @return a {@link String} object.
+ * @throws IllegalDataAddressException if any.
+ */
+ public String getString(int range, int offset, int dataType, int registerCount, Charset charset)
+ throws IllegalDataAddressException {
+ return getRegister(new StringLocator(slaveId, range, offset, dataType, registerCount, charset));
+ }
+
+ /**
+ * getRegister.
+ *
+ * @param locator a {@link BaseLocator} object.
+ * @param a T object.
+ * @return a T object.
+ * @throws IllegalDataAddressException if any.
+ */
+ public synchronized T getRegister(BaseLocator locator) throws IllegalDataAddressException {
+ int words = locator.getRegisterCount();
+ byte[] b = new byte[locator.getRegisterCount() * 2];
+ for (int i = 0; i < words; i++) {
+ short s;
+ if (locator.getRange() == RegisterRange.INPUT_REGISTER)
+ s = getInputRegister(locator.getOffset() + i);
+ else if (locator.getRange() == RegisterRange.HOLDING_REGISTER)
+ s = getHoldingRegister(locator.getOffset() + i);
+ else if (allowInvalidAddress)
+ s = invalidAddressValue;
+ else
+ throw new IllegalDataAddressException();
+ b[i * 2] = ModbusUtils.toByte(s, true);
+ b[i * 2 + 1] = ModbusUtils.toByte(s, false);
+ }
+
+ return locator.bytesToValueRealOffset(b, 0);
+ }
+
+ @Override
+ public synchronized boolean getCoil(int offset) throws IllegalDataAddressException {
+ return getBoolean(offset, coils);
+ }
+
+ //
+ //
+ // ProcessImage interface
+ //
+
+ //
+ // Coils
+
+ @Override
+ public synchronized void setCoil(int offset, boolean value) {
+ validateOffset(offset);
+ coils.put(offset, value);
+ }
+
+ @Override
+ public synchronized void writeCoil(int offset, boolean value) throws IllegalDataAddressException {
+ boolean old = getBoolean(offset, coils);
+ setCoil(offset, value);
+
+ for (ProcessImageListener l : writeListeners)
+ l.coilWrite(offset, old, value);
+ }
+
+ @Override
+ public synchronized boolean getInput(int offset) throws IllegalDataAddressException {
+ return getBoolean(offset, inputs);
+ }
+
+ //
+ // Inputs
+
+ @Override
+ public synchronized void setInput(int offset, boolean value) {
+ validateOffset(offset);
+ inputs.put(offset, value);
+ }
+
+ @Override
+ public synchronized short getHoldingRegister(int offset) throws IllegalDataAddressException {
+ return getShort(offset, holdingRegisters);
+ }
+
+ //
+ // Holding registers
+
+ @Override
+ public synchronized void setHoldingRegister(int offset, short value) {
+ validateOffset(offset);
+ holdingRegisters.put(offset, value);
+ }
+
+ @Override
+ public synchronized void writeHoldingRegister(int offset, short value) throws IllegalDataAddressException {
+ short old = getShort(offset, holdingRegisters);
+ setHoldingRegister(offset, value);
+
+ for (ProcessImageListener l : writeListeners)
+ l.holdingRegisterWrite(offset, old, value);
+ }
+
+ @Override
+ public synchronized short getInputRegister(int offset) throws IllegalDataAddressException {
+ return getShort(offset, inputRegisters);
+ }
+
+ //
+ // Input registers
+
+ @Override
+ public synchronized void setInputRegister(int offset, short value) {
+ validateOffset(offset);
+ inputRegisters.put(offset, value);
+ }
+
+ @Override
+ public byte getExceptionStatus() {
+ return exceptionStatus;
+ }
+
+ //
+ // Exception status
+
+ /**
+ * Setter for the field exceptionStatus.
+ *
+ * @param exceptionStatus a byte.
+ */
+ public void setExceptionStatus(byte exceptionStatus) {
+ this.exceptionStatus = exceptionStatus;
+ }
+
+ //
+ // Report slave id
+
+ @Override
+ public byte[] getReportSlaveIdData() {
+ return new byte[0];
+ }
+
+ //
+ //
+ // Private
+ //
+ private short getShort(int offset, Map map) throws IllegalDataAddressException {
+ Short value = map.get(offset);
+ if (value == null) {
+ if (allowInvalidAddress)
+ return invalidAddressValue;
+ throw new IllegalDataAddressException();
+ }
+ return value.shortValue();
+ }
+
+ private boolean getBoolean(int offset, Map map) throws IllegalDataAddressException {
+ Boolean value = map.get(offset);
+ if (value == null) {
+ if (allowInvalidAddress)
+ return false;
+ throw new IllegalDataAddressException();
+ }
+ return value.booleanValue();
+ }
+
+ private void validateOffset(int offset) {
+ if (offset < 0 || offset > 65535)
+ throw new ModbusIdException("Invalid offset: " + offset);
+ }
+
+ private void validateBit(int bit) {
+ if (bit < 0 || bit > 15)
+ throw new ModbusIdException("Invalid bit: " + bit);
+ }
+
+ private short setBit(short s, int bit, boolean value) {
+ return (short) (s | ((value ? 1 : 0) << bit));
+ }
+
+ private boolean getBit(short s, int bit) {
+ return ((s >> bit) & 0x1) == 1;
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/BatchRead.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/BatchRead.java
new file mode 100644
index 0000000..0299bbe
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/BatchRead.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.KeyedModbusLocator;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.ReadFunctionGroup;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.SlaveAndRange;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.locator.BaseLocator;
+
+import java.util.*;
+
+/**
+ * A class for defining the information required to obtain in a batch.
+ *
+ * The generic parameterization represents the class of the key that will be used to find the results in the BatchRead
+ * object. Typically String would be used, but any Object is valid.
+ *
+ * Some modbus devices have non-contiguous sets of values within a single register range. These gaps between values may
+ * cause the device to return error responses if a request attempts to read them. In spite of this, because it is
+ * generally more efficient to read a set of values with a single request, the batch read by default will assume that no
+ * such error responses will be returned. If your batch request results in such errors, it is recommended that you
+ * separate the offending request to a separate batch read object, or you can use the "contiguous requests" setting
+ * which causes requests to be partitioned into only contiguous sets.
+ *
+ * @param - Type of read
+ * @author mlohbihler
+ * @version 2025.9.0
+ */
+public class BatchRead {
+ private final List> requestValues = new ArrayList<>();
+
+ /**
+ * See documentation above.
+ */
+ private boolean contiguousRequests = false;
+
+ /**
+ * If this value is false, any error response received will cause an exception to be thrown, and the entire batch to
+ * be aborted (unless exceptionsInResults is true - see below). If set to true, error responses will be set as the
+ * result of all affected locators and the entire batch will be attempted with no such exceptions thrown.
+ */
+ private boolean errorsInResults = false;
+
+ /**
+ * If this value is false, any exceptions thrown will cause the entire batch to be aborted. If set to true, the
+ * exception will be set as the result of all affected locators and the entire batch will be attempted with no such
+ * exceptions thrown.
+ */
+ private boolean exceptionsInResults = false;
+
+ /**
+ * A batch may be split into an arbitrary number of individual Modbus requests, and so a given batch may take
+ * an arbitrary amount of time to complete. The cancel field is provided to allow the batch to be cancelled.
+ */
+ private boolean cancel;
+
+ /**
+ * This is what the data looks like after partitioning.
+ */
+ private List> functionGroups;
+
+ /**
+ * isContiguousRequests.
+ *
+ * @return a boolean.
+ */
+ public boolean isContiguousRequests() {
+ return contiguousRequests;
+ }
+
+ /**
+ * Setter for the field contiguousRequests.
+ *
+ * @param contiguousRequests a boolean.
+ */
+ public void setContiguousRequests(boolean contiguousRequests) {
+ this.contiguousRequests = contiguousRequests;
+ functionGroups = null;
+ }
+
+ /**
+ * isErrorsInResults.
+ *
+ * @return a boolean.
+ */
+ public boolean isErrorsInResults() {
+ return errorsInResults;
+ }
+
+ /**
+ * Setter for the field errorsInResults.
+ *
+ * @param errorsInResults a boolean.
+ */
+ public void setErrorsInResults(boolean errorsInResults) {
+ this.errorsInResults = errorsInResults;
+ }
+
+ /**
+ * isExceptionsInResults.
+ *
+ * @return a boolean.
+ */
+ public boolean isExceptionsInResults() {
+ return exceptionsInResults;
+ }
+
+ /**
+ * Setter for the field exceptionsInResults.
+ *
+ * @param exceptionsInResults a boolean.
+ */
+ public void setExceptionsInResults(boolean exceptionsInResults) {
+ this.exceptionsInResults = exceptionsInResults;
+ }
+
+ /**
+ * getReadFunctionGroups.
+ *
+ * @param master a {@link ModbusMaster} object.
+ * @return a {@link List} object.
+ */
+ public List> getReadFunctionGroups(ModbusMaster master) {
+ if (functionGroups == null)
+ doPartition(master);
+ return functionGroups;
+ }
+
+ /**
+ * addLocator.
+ *
+ * @param id a K object.
+ * @param locator a {@link BaseLocator} object.
+ */
+ public void addLocator(K id, BaseLocator> locator) {
+ addLocator(new KeyedModbusLocator<>(id, locator));
+ }
+
+ private void addLocator(KeyedModbusLocator locator) {
+ requestValues.add(locator);
+ functionGroups = null;
+ }
+
+ /**
+ * isCancel.
+ *
+ * @return a boolean.
+ */
+ public boolean isCancel() {
+ return cancel;
+ }
+
+ /**
+ * Setter for the field cancel.
+ *
+ * @param cancel a boolean.
+ */
+ public void setCancel(boolean cancel) {
+ this.cancel = cancel;
+ }
+
+ //
+ //
+ // Private stuff
+ //
+ private void doPartition(ModbusMaster master) {
+ Map>> slaveRangeBatch = new HashMap<>();
+
+ // Separate the batch into slave ids and read functions.
+ List> functions;
+ for (KeyedModbusLocator locator : requestValues) {
+ // Find the function list for this slave and range. Create it if necessary.
+ functions = slaveRangeBatch.computeIfAbsent(locator.getSlaveAndRange(), k -> new ArrayList<>());
+
+ // Add this locator to the function list.
+ functions.add(locator);
+ }
+
+ // Now that we have locators grouped into slave and function, check each read function group and break into
+ // parts as necessary.
+ Collection>> functionLocatorLists = slaveRangeBatch.values();
+ FunctionLocatorComparator comparator = new FunctionLocatorComparator();
+ functionGroups = new ArrayList<>();
+ for (List> functionLocatorList : functionLocatorLists) {
+ // Sort the list by offset.
+ Collections.sort(functionLocatorList, comparator);
+
+ // Break into parts by excessive request length. Remember the max item count that we can ask for, for
+ // this function
+ int maxReadCount = master.getMaxReadCount(functionLocatorList.get(0).getSlaveAndRange().getRange());
+
+ // Create the request groups.
+ createRequestGroups(functionGroups, functionLocatorList, maxReadCount);
+ //System.out.println("requests: " + functionGroups.size());
+ }
+ }
+
+ /**
+ * We aren't trying to do anything fancy here, like some kind of artificial optimal group for performance or
+ * anything. We pretty much just try to fit as many locators as possible into a single valid request, and then move
+ * on.
+ *
+ * This method assumes the locators have already been sorted by start offset.
+ */
+ private void createRequestGroups(List> functionGroups, List> locators,
+ int maxCount) {
+ ReadFunctionGroup functionGroup;
+ KeyedModbusLocator locator;
+ int index;
+ int endOffset;
+ // Loop for creation of groups.
+ while (locators.size() > 0) {
+ functionGroup = new ReadFunctionGroup<>(locators.remove(0));
+ functionGroups.add(functionGroup);
+ endOffset = functionGroup.getStartOffset() + maxCount - 1;
+
+ // Loop for adding locators to the current group
+ index = 0;
+ while (locators.size() > index) {
+ locator = locators.get(index);
+ boolean added = false;
+
+ if (locator.getEndOffset() <= endOffset) {
+ if (contiguousRequests) {
+ // The locator must at least abut the other locators in the group.
+ if (locator.getOffset() <= functionGroup.getEndOffset() + 1) {
+ functionGroup.add(locators.remove(index));
+ added = true;
+ }
+ } else {
+ functionGroup.add(locators.remove(index));
+ added = true;
+ }
+ }
+
+ if (!added) {
+ // This locator doesn't fit inside the current function...
+ if (locator.getOffset() > endOffset)
+ // ... and since the list is sorted by offset, no other locators can either, so quit the loop.
+ break;
+
+ // ... but there still may be other locators that can, so increment the index
+ index++;
+ }
+ }
+ }
+ }
+
+ class FunctionLocatorComparator implements Comparator> {
+ @Override
+ public int compare(KeyedModbusLocator ml1, KeyedModbusLocator ml2) {
+ return ml1.getOffset() - ml2.getOffset();
+ }
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/BatchResults.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/BatchResults.java
new file mode 100644
index 0000000..21ac045
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/BatchResults.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * BatchResults class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class BatchResults {
+ private final Map data = new HashMap<>();
+
+ /**
+ * addResult.
+ *
+ * @param key a K object.
+ * @param value a {@link Object} object.
+ */
+ public void addResult(K key, Object value) {
+ data.put(key, value);
+ }
+
+ /**
+ * getValue.
+ *
+ * @param key a K object.
+ * @return a {@link Object} object.
+ */
+ public Object getValue(K key) {
+ return data.get(key);
+ }
+
+ /**
+ * getIntValue.
+ *
+ * @param key a K object.
+ * @return a {@link Integer} object.
+ */
+ public Integer getIntValue(K key) {
+ return (Integer) getValue(key);
+ }
+
+ /**
+ * getLongValue.
+ *
+ * @param key a K object.
+ * @return a {@link Long} object.
+ */
+ public Long getLongValue(K key) {
+ return (Long) getValue(key);
+ }
+
+ /**
+ * getDoubleValue.
+ *
+ * @param key a K object.
+ * @return a {@link Double} object.
+ */
+ public Double getDoubleValue(K key) {
+ return (Double) getValue(key);
+ }
+
+ /**
+ * getFloatValue.
+ *
+ * @param key a K object.
+ * @return a {@link Float} object.
+ */
+ public Float getFloatValue(K key) {
+ return (Float) getValue(key);
+ }
+
+
+ @Override
+ public String toString() {
+ return data.toString();
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ExceptionResult.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ExceptionResult.java
new file mode 100644
index 0000000..067b7ae
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ExceptionResult.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.ExceptionCode;
+
+/**
+ * ExceptionResult class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class ExceptionResult {
+ private final byte exceptionCode;
+ private final String exceptionMessage;
+
+ /**
+ * Constructor for ExceptionResult.
+ *
+ * @param exceptionCode a byte.
+ */
+ public ExceptionResult(byte exceptionCode) {
+ this.exceptionCode = exceptionCode;
+ exceptionMessage = ExceptionCode.getExceptionMessage(exceptionCode);
+ }
+
+ /**
+ * Getter for the field exceptionCode.
+ *
+ * @return a byte.
+ */
+ public byte getExceptionCode() {
+ return exceptionCode;
+ }
+
+ /**
+ * Getter for the field exceptionMessage.
+ *
+ * @return a {@link String} object.
+ */
+ public String getExceptionMessage() {
+ return exceptionMessage;
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/Modbus.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/Modbus.java
new file mode 100644
index 0000000..f4fbaf9
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/Modbus.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.RegisterRange;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.DefaultMessagingExceptionHandler;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.MessagingExceptionHandler;
+
+/**
+ * Base level for masters and slaves/listeners
+ *
+ * TODO: - handle echoing in RS485
+ *
+ * @author mlohbihler
+ * @version 2025.9.0
+ */
+public class Modbus {
+ /**
+ * Constant DEFAULT_MAX_READ_BIT_COUNT=2000
+ */
+ public static final int DEFAULT_MAX_READ_BIT_COUNT = 2000;
+ /**
+ * Constant DEFAULT_MAX_READ_REGISTER_COUNT=125
+ */
+ public static final int DEFAULT_MAX_READ_REGISTER_COUNT = 125;
+ /**
+ * Constant DEFAULT_MAX_WRITE_REGISTER_COUNT=120
+ */
+ public static final int DEFAULT_MAX_WRITE_REGISTER_COUNT = 120;
+
+ private MessagingExceptionHandler exceptionHandler = new DefaultMessagingExceptionHandler();
+
+ private int maxReadBitCount = DEFAULT_MAX_READ_BIT_COUNT;
+ private int maxReadRegisterCount = DEFAULT_MAX_READ_REGISTER_COUNT;
+ private int maxWriteRegisterCount = DEFAULT_MAX_WRITE_REGISTER_COUNT;
+
+ /**
+ *
getMaxReadCount.
+ *
+ * @param registerRange a int.
+ * @return a int.
+ */
+ public int getMaxReadCount(int registerRange) {
+ switch (registerRange) {
+ case RegisterRange.COIL_STATUS:
+ case RegisterRange.INPUT_STATUS:
+ return maxReadBitCount;
+ case RegisterRange.HOLDING_REGISTER:
+ case RegisterRange.INPUT_REGISTER:
+ return maxReadRegisterCount;
+ }
+ return -1;
+ }
+
+ /**
+ * validateNumberOfBits.
+ *
+ * @param bits a int.
+ * @throws ModbusTransportException if any.
+ */
+ public void validateNumberOfBits(int bits) throws ModbusTransportException {
+ if (bits < 1 || bits > maxReadBitCount)
+ throw new ModbusTransportException("Invalid number of bits: " + bits);
+ }
+
+ /**
+ * validateNumberOfRegisters.
+ *
+ * @param registers a int.
+ * @throws ModbusTransportException if any.
+ */
+ public void validateNumberOfRegisters(int registers) throws ModbusTransportException {
+ if (registers < 1 || registers > maxReadRegisterCount)
+ throw new ModbusTransportException("Invalid number of registers: " + registers);
+ }
+
+ /**
+ * Getter for the field exceptionHandler.
+ *
+ * @return a {@link MessagingExceptionHandler} object.
+ */
+ public MessagingExceptionHandler getExceptionHandler() {
+ return exceptionHandler;
+ }
+
+ /**
+ * Setter for the field exceptionHandler.
+ *
+ * @param exceptionHandler a {@link MessagingExceptionHandler} object.
+ */
+ public void setExceptionHandler(MessagingExceptionHandler exceptionHandler) {
+ if (exceptionHandler == null)
+ this.exceptionHandler = new DefaultMessagingExceptionHandler();
+ else
+ this.exceptionHandler = exceptionHandler;
+ }
+
+ /**
+ * Getter for the field maxReadBitCount.
+ *
+ * @return a int.
+ */
+ public int getMaxReadBitCount() {
+ return maxReadBitCount;
+ }
+
+ /**
+ * Setter for the field maxReadBitCount.
+ *
+ * @param maxReadBitCount a int.
+ */
+ public void setMaxReadBitCount(int maxReadBitCount) {
+ this.maxReadBitCount = maxReadBitCount;
+ }
+
+ /**
+ * Getter for the field maxReadRegisterCount.
+ *
+ * @return a int.
+ */
+ public int getMaxReadRegisterCount() {
+ return maxReadRegisterCount;
+ }
+
+ /**
+ * Setter for the field maxReadRegisterCount.
+ *
+ * @param maxReadRegisterCount a int.
+ */
+ public void setMaxReadRegisterCount(int maxReadRegisterCount) {
+ this.maxReadRegisterCount = maxReadRegisterCount;
+ }
+
+ /**
+ * Getter for the field maxWriteRegisterCount.
+ *
+ * @return a int.
+ */
+ public int getMaxWriteRegisterCount() {
+ return maxWriteRegisterCount;
+ }
+
+ /**
+ * Setter for the field maxWriteRegisterCount.
+ *
+ * @param maxWriteRegisterCount a int.
+ */
+ public void setMaxWriteRegisterCount(int maxWriteRegisterCount) {
+ this.maxWriteRegisterCount = maxWriteRegisterCount;
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ModbusFactory.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ModbusFactory.java
new file mode 100644
index 0000000..968a611
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ModbusFactory.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.ModbusUtils;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.RegisterRange;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusIdException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.IpParameters;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.listener.TcpListener;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.tcp.TcpMaster;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.tcp.TcpSlave;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.udp.UdpMaster;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.udp.UdpSlave;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.*;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.serial.SerialPortWrapper;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.serial.ascii.AsciiMaster;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.serial.ascii.AsciiSlave;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.serial.rtu.RtuMaster;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.serial.rtu.RtuSlave;
+
+/**
+ * ModbusFactory class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class ModbusFactory {
+ //
+ // Modbus masters
+ //
+
+ /**
+ * createRtuMaster.
+ *
+ * @param wrapper a {@link SerialPortWrapper} object.
+ * @return a {@link ModbusMaster} object.
+ */
+ public ModbusMaster createRtuMaster(SerialPortWrapper wrapper) {
+ return new RtuMaster(wrapper);
+ }
+
+ /**
+ * createAsciiMaster.
+ *
+ * @param wrapper a {@link SerialPortWrapper} object.
+ * @return a {@link ModbusMaster} object.
+ */
+ public ModbusMaster createAsciiMaster(SerialPortWrapper wrapper) {
+ return new AsciiMaster(wrapper);
+ }
+
+ /**
+ * createTcpMaster.
+ *
+ * @param params a {@link IpParameters} object.
+ * @param keepAlive a boolean.
+ * @return a {@link ModbusMaster} object.
+ */
+ public ModbusMaster createTcpMaster(IpParameters params, boolean keepAlive) {
+ return new TcpMaster(params, keepAlive);
+ }
+
+ /**
+ * createTcpMaster.
+ *
+ * @param params a {@link IpParameters} object.
+ * @param keepAlive a boolean.
+ * @param lingerTime an Integer.
+ * @return a {@link ModbusMaster} object.
+ */
+ public ModbusMaster createTcpMaster(IpParameters params, boolean keepAlive, Integer lingerTime) {
+ return new TcpMaster(params, keepAlive, lingerTime);
+ }
+
+ /**
+ * createUdpMaster.
+ *
+ * @param params a {@link IpParameters} object.
+ * @return a {@link ModbusMaster} object.
+ */
+ public ModbusMaster createUdpMaster(IpParameters params) {
+ return new UdpMaster(params);
+ }
+
+ /**
+ * createTcpListener.
+ *
+ * @param params a {@link IpParameters} object.
+ * @return a {@link ModbusMaster} object.
+ */
+ public ModbusMaster createTcpListener(IpParameters params) {
+ return new TcpListener(params);
+ }
+
+ //
+ // Modbus slaves
+ //
+
+ /**
+ * createRtuSlave.
+ *
+ * @param wrapper a {@link SerialPortWrapper} object.
+ * @return a {@link ModbusSlaveSet} object.
+ */
+ public ModbusSlaveSet createRtuSlave(SerialPortWrapper wrapper) {
+ return new RtuSlave(wrapper);
+ }
+
+ /**
+ * createAsciiSlave.
+ *
+ * @param wrapper a {@link SerialPortWrapper} object.
+ * @return a {@link ModbusSlaveSet} object.
+ */
+ public ModbusSlaveSet createAsciiSlave(SerialPortWrapper wrapper) {
+ return new AsciiSlave(wrapper);
+ }
+
+ /**
+ * createTcpSlave.
+ *
+ * @param encapsulated a boolean.
+ * @return a {@link ModbusSlaveSet} object.
+ */
+ public ModbusSlaveSet createTcpSlave(boolean encapsulated) {
+ return new TcpSlave(encapsulated);
+ }
+
+ /**
+ * createUdpSlave.
+ *
+ * @param encapsulated a boolean.
+ * @return a {@link ModbusSlaveSet} object.
+ */
+ public ModbusSlaveSet createUdpSlave(boolean encapsulated) {
+ return new UdpSlave(encapsulated);
+ }
+
+ //
+ // Modbus requests
+ //
+
+ /**
+ * createReadRequest.
+ *
+ * @param slaveId a int.
+ * @param range a int.
+ * @param offset a int.
+ * @param length a int.
+ * @return a {@link ModbusRequest} object.
+ * @throws ModbusTransportException if any.
+ * @throws ModbusIdException if any.
+ */
+ public ModbusRequest createReadRequest(int slaveId, int range, int offset, int length)
+ throws ModbusTransportException, ModbusIdException {
+ ModbusUtils.validateRegisterRange(range);
+
+ if (range == RegisterRange.COIL_STATUS)
+ return new ReadCoilsRequest(slaveId, offset, length);
+
+ if (range == RegisterRange.INPUT_STATUS)
+ return new ReadDiscreteInputsRequest(slaveId, offset, length);
+
+ if (range == RegisterRange.INPUT_REGISTER)
+ return new ReadInputRegistersRequest(slaveId, offset, length);
+
+ return new ReadHoldingRegistersRequest(slaveId, offset, length);
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ModbusMaster.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ModbusMaster.java
new file mode 100644
index 0000000..3b9fe70
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ModbusMaster.java
@@ -0,0 +1,573 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.KeyedModbusLocator;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.ReadFunctionGroup;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.SlaveProfile;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.DataType;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.ExceptionCode;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.FunctionCode;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.RegisterRange;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ErrorResponseException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.InvalidDataConversionException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusInitException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.locator.BaseLocator;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.locator.BinaryLocator;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.locator.NumericLocator;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.*;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.epoll.InputStreamEPollWrapper;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.log.BaseIOLog;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.MessageControl;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.ArrayUtils;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.ProgressiveTask;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Abstract ModbusMaster class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+abstract public class ModbusMaster extends Modbus {
+ private final Map slaveProfiles = new HashMap<>();
+ /**
+ * Should we validate the responses:
+ * - ensure that the requested slave id is what is in the response
+ */
+ protected boolean validateResponse;
+ /**
+ * If connection is established with slave/slaves
+ */
+ protected boolean connected = false;
+ protected boolean initialized;
+ private int timeout = 500;
+ private int retries = 2;
+ /**
+ * If the slave equipment only supports multiple write commands, set this to true. Otherwise, and combination of
+ * single or multiple write commands will be used as appropriate.
+ */
+ private boolean multipleWritesOnly;
+
+ private int discardDataDelay = 0;
+ private BaseIOLog ioLog;
+
+ /**
+ * An input stream ePoll will use a single thread to read all input streams. If multiple serial or TCP modbus
+ * connections are to be made, an ePoll can be much more efficient.
+ */
+ private InputStreamEPollWrapper ePoll;
+
+ /**
+ * isConnected.
+ *
+ * @return a boolean.
+ */
+ public boolean isConnected() {
+ return connected;
+ }
+
+ /**
+ * Setter for the field connected.
+ *
+ * @param connected a boolean.
+ */
+ public void setConnected(boolean connected) {
+ this.connected = connected;
+ }
+
+ /**
+ * init.
+ *
+ * @throws ModbusInitException if any.
+ */
+ abstract public void init() throws ModbusInitException;
+
+ /**
+ * isInitialized.
+ *
+ * @return a boolean.
+ */
+ public boolean isInitialized() {
+ return initialized;
+ }
+
+ /**
+ * destroy.
+ */
+ abstract public void destroy();
+
+ /**
+ * send.
+ *
+ * @param request a {@link ModbusRequest} object.
+ * @return a {@link ModbusResponse} object.
+ * @throws ModbusTransportException if any.
+ */
+ public final ModbusResponse send(ModbusRequest request) throws ModbusTransportException {
+ request.validate(this);
+ ModbusResponse modbusResponse = sendImpl(request);
+ if (validateResponse)
+ modbusResponse.validateResponse(request);
+ return modbusResponse;
+ }
+
+ /**
+ * sendImpl.
+ *
+ * @param request a {@link ModbusRequest} object.
+ * @return a {@link ModbusResponse} object.
+ * @throws ModbusTransportException if any.
+ */
+ abstract public ModbusResponse sendImpl(ModbusRequest request) throws ModbusTransportException;
+
+ /**
+ * Returns a value from the modbus network according to the given locator information. Various data types are
+ * allowed to be requested including multi-word types. The determination of the correct request message to send is
+ * handled automatically.
+ *
+ * @param locator the information required to locate the value in the modbus network.
+ * @param a T object.
+ * @return an object representing the value found. This will be one of Boolean, Short, Integer, Long, BigInteger,
+ * Float, or Double. See the DataType enumeration for details on which type to expect.
+ * @throws ModbusTransportException if there was an IO error or other technical failure while sending the message
+ * @throws ErrorResponseException if the response returned from the slave was an exception.
+ */
+ @SuppressWarnings("unchecked")
+ public T getValue(BaseLocator locator) throws ModbusTransportException, ErrorResponseException {
+ BatchRead batch = new BatchRead<>();
+ batch.addLocator("", locator);
+ BatchResults result = send(batch);
+ return (T) result.getValue("");
+ }
+
+ /**
+ * Sets the given value in the modbus network according to the given locator information. Various data types are
+ * allowed to be set including including multi-word types. The determination of the correct write message to send is
+ * handled automatically.
+ *
+ * @param locator the information required to locate the value in the modbus network.
+ * @param value an object representing the value to be set. This will be one of Boolean, Short, Integer, Long, BigInteger,
+ * Float, or Double. See the DataType enumeration for details on which type to expect.
+ * @param type of locator
+ * @throws ModbusTransportException if there was an IO error or other technical failure while sending the message
+ * @throws ErrorResponseException if the response returned from the slave was an exception.
+ */
+ public void setValue(BaseLocator locator, Object value) throws ModbusTransportException,
+ ErrorResponseException {
+ int slaveId = locator.getSlaveId();
+ int registerRange = locator.getRange();
+ int writeOffset = locator.getOffset();
+
+ // Determine the request type that we will use
+ if (registerRange == RegisterRange.INPUT_STATUS || registerRange == RegisterRange.INPUT_REGISTER)
+ throw new RuntimeException("Cannot write to input status or input register ranges");
+
+ if (registerRange == RegisterRange.COIL_STATUS) {
+ if (!(value instanceof Boolean))
+ throw new InvalidDataConversionException("Only boolean values can be written to coils");
+ if (multipleWritesOnly)
+ setValue(new WriteCoilsRequest(slaveId, writeOffset, new boolean[]{((Boolean) value).booleanValue()}));
+ else
+ setValue(new WriteCoilRequest(slaveId, writeOffset, ((Boolean) value).booleanValue()));
+ } else {
+ // Writing to holding registers.
+ if (locator.getDataType() == DataType.BINARY) {
+ if (!(value instanceof Boolean))
+ throw new InvalidDataConversionException("Only boolean values can be written to coils");
+ setHoldingRegisterBit(slaveId, writeOffset, ((BinaryLocator) locator).getBit(),
+ ((Boolean) value).booleanValue());
+ } else {
+ // Writing some kind of value to a holding register.
+ @SuppressWarnings("unchecked")
+ short[] data = locator.valueToShorts((T) value);
+ if (data.length == 1 && !multipleWritesOnly)
+ setValue(new WriteRegisterRequest(slaveId, writeOffset, data[0]));
+ else
+ setValue(new WriteRegistersRequest(slaveId, writeOffset, data));
+ }
+ }
+
+ }
+
+ /**
+ * Node scanning. Returns a list of slave nodes that respond to a read exception status request (perhaps with an
+ * error, but respond nonetheless).
+ *
+ * Note: a similar scan could be done for registers in nodes, but, for one thing, it would take some time to run,
+ * and in any case the results would not be meaningful since there would be no semantic information accompanying the
+ * results.
+ *
+ * @return a {@link List} object.
+ */
+ public List scanForSlaveNodes() {
+ List result = new ArrayList<>();
+ for (int i = 1; i <= 240; i++) {
+ if (testSlaveNode(i))
+ result.add(i);
+ }
+ return result;
+ }
+
+ /**
+ * scanForSlaveNodes.
+ *
+ * @param l a {@link NodeScanListener} object.
+ * @return a {@link ProgressiveTask} object.
+ */
+ public ProgressiveTask scanForSlaveNodes(final NodeScanListener l) {
+ l.progressUpdate(0);
+ ProgressiveTask task = new ProgressiveTask(l) {
+ private int node = 1;
+
+ @Override
+ protected void runImpl() {
+ if (testSlaveNode(node))
+ l.nodeFound(node);
+
+ declareProgress(((float) node) / 240);
+
+ node++;
+ if (node > 240)
+ completed = true;
+ }
+ };
+
+ new Thread(task).start();
+
+ return task;
+ }
+
+ /**
+ * testSlaveNode.
+ *
+ * @param node a int.
+ * @return a boolean.
+ */
+ public boolean testSlaveNode(int node) {
+ try {
+ send(new ReadHoldingRegistersRequest(node, 0, 1));
+ } catch (ModbusTransportException e) {
+ // If there was a transport exception, there's no node there.
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Getter for the field retries.
+ *
+ * @return a int.
+ */
+ public int getRetries() {
+ return retries;
+ }
+
+ /**
+ * Setter for the field retries.
+ *
+ * @param retries a int.
+ */
+ public void setRetries(int retries) {
+ if (retries < 0)
+ this.retries = 0;
+ else
+ this.retries = retries;
+ }
+
+ /**
+ * Getter for the field timeout.
+ *
+ * @return a int.
+ */
+ public int getTimeout() {
+ return timeout;
+ }
+
+ /**
+ * Setter for the field timeout.
+ *
+ * @param timeout a int.
+ */
+ public void setTimeout(int timeout) {
+ if (timeout < 1)
+ this.timeout = 1;
+ else
+ this.timeout = timeout;
+ }
+
+ /**
+ * isMultipleWritesOnly.
+ *
+ * @return a boolean.
+ */
+ public boolean isMultipleWritesOnly() {
+ return multipleWritesOnly;
+ }
+
+ /**
+ * Setter for the field multipleWritesOnly.
+ *
+ * @param multipleWritesOnly a boolean.
+ */
+ public void setMultipleWritesOnly(boolean multipleWritesOnly) {
+ this.multipleWritesOnly = multipleWritesOnly;
+ }
+
+ /**
+ * Getter for the field discardDataDelay.
+ *
+ * @return a int.
+ */
+ public int getDiscardDataDelay() {
+ return discardDataDelay;
+ }
+
+ /**
+ * Setter for the field discardDataDelay.
+ *
+ * @param discardDataDelay a int.
+ */
+ public void setDiscardDataDelay(int discardDataDelay) {
+ if (discardDataDelay < 0)
+ this.discardDataDelay = 0;
+ else
+ this.discardDataDelay = discardDataDelay;
+ }
+
+ /**
+ * Getter for the field ioLog.
+ *
+ * @return a {@link BaseIOLog} object.
+ */
+ public BaseIOLog getIoLog() {
+ return ioLog;
+ }
+
+ /**
+ * Setter for the field ioLog.
+ *
+ * @param ioLog a {@link BaseIOLog} object.
+ */
+ public void setIoLog(BaseIOLog ioLog) {
+ this.ioLog = ioLog;
+ }
+
+ /**
+ * Getter for the field ePoll.
+ *
+ * @return a {@link InputStreamEPollWrapper} object.
+ */
+ public InputStreamEPollWrapper getePoll() {
+ return ePoll;
+ }
+
+ /**
+ * Setter for the field ePoll.
+ *
+ * @param ePoll a {@link InputStreamEPollWrapper} object.
+ */
+ public void setePoll(InputStreamEPollWrapper ePoll) {
+ this.ePoll = ePoll;
+ }
+
+ /**
+ * Useful for sending a number of polling commands at once, or at least in as optimal a batch as possible.
+ *
+ * @param batch a {@link BatchRead} object.
+ * @param type of result
+ * @return a {@link BatchResults} object.
+ * @throws ModbusTransportException if any.
+ * @throws ErrorResponseException if any.
+ */
+ public BatchResults send(BatchRead batch) throws ModbusTransportException, ErrorResponseException {
+ if (!initialized)
+ throw new ModbusTransportException("not initialized");
+
+ BatchResults results = new BatchResults<>();
+ List> functionGroups = batch.getReadFunctionGroups(this);
+
+ // Execute each read function and process the results.
+ for (ReadFunctionGroup functionGroup : functionGroups) {
+ sendFunctionGroup(functionGroup, results, batch.isErrorsInResults(), batch.isExceptionsInResults());
+ if (batch.isCancel())
+ break;
+ }
+
+ return results;
+ }
+
+ //
+ //
+ // Protected methods
+ //
+
+ /**
+ * getMessageControl.
+ *
+ * @return a {@link MessageControl} object.
+ */
+ protected MessageControl getMessageControl() {
+ MessageControl conn = new MessageControl();
+ conn.setRetries(getRetries());
+ conn.setTimeout(getTimeout());
+ conn.setDiscardDataDelay(getDiscardDataDelay());
+ conn.setExceptionHandler(getExceptionHandler());
+ conn.setIoLog(ioLog);
+ return conn;
+ }
+
+ /**
+ * closeMessageControl.
+ *
+ * @param conn a {@link MessageControl} object.
+ */
+ protected void closeMessageControl(MessageControl conn) {
+ if (conn != null)
+ conn.close();
+ }
+
+ //
+ //
+ // Private stuff
+ //
+
+ /**
+ * This method assumes that all locators have already been pre-sorted and grouped into valid requests, say, by the
+ * createRequestGroups method.
+ */
+ private void sendFunctionGroup(ReadFunctionGroup functionGroup, BatchResults results,
+ boolean errorsInResults, boolean exceptionsInResults) throws ModbusTransportException,
+ ErrorResponseException {
+ int slaveId = functionGroup.getSlaveAndRange().getSlaveId();
+ int startOffset = functionGroup.getStartOffset();
+ int length = functionGroup.getLength();
+
+ // Inspect the function group for data required to create the request.
+ ModbusRequest request;
+ if (functionGroup.getFunctionCode() == FunctionCode.READ_COILS)
+ request = new ReadCoilsRequest(slaveId, startOffset, length);
+ else if (functionGroup.getFunctionCode() == FunctionCode.READ_DISCRETE_INPUTS)
+ request = new ReadDiscreteInputsRequest(slaveId, startOffset, length);
+ else if (functionGroup.getFunctionCode() == FunctionCode.READ_HOLDING_REGISTERS)
+ request = new ReadHoldingRegistersRequest(slaveId, startOffset, length);
+ else if (functionGroup.getFunctionCode() == FunctionCode.READ_INPUT_REGISTERS)
+ request = new ReadInputRegistersRequest(slaveId, startOffset, length);
+ else
+ throw new RuntimeException("Unsupported function");
+
+ ReadResponse response;
+ try {
+ response = (ReadResponse) send(request);
+ } catch (ModbusTransportException e) {
+ if (!exceptionsInResults)
+ throw e;
+
+ for (KeyedModbusLocator locator : functionGroup.getLocators())
+ results.addResult(locator.getKey(), e);
+
+ return;
+ }
+
+ byte[] data = null;
+ if (!errorsInResults && response.isException())
+ throw new ErrorResponseException(request, response);
+ else if (!response.isException())
+ data = response.getData();
+
+ for (KeyedModbusLocator locator : functionGroup.getLocators()) {
+ if (errorsInResults && response.isException())
+ results.addResult(locator.getKey(), new ExceptionResult(response.getExceptionCode()));
+ else {
+ try {
+ results.addResult(locator.getKey(), locator.bytesToValue(data, startOffset));
+ } catch (RuntimeException e) {
+ throw new RuntimeException("Result conversion exception. data=" + ArrayUtils.toHexString(data)
+ + ", startOffset=" + startOffset + ", locator=" + locator + ", functionGroup.functionCode="
+ + functionGroup.getFunctionCode() + ", functionGroup.startOffset=" + startOffset
+ + ", functionGroup.length=" + length, e);
+ }
+ }
+ }
+ }
+
+ private void setValue(ModbusRequest request) throws ModbusTransportException, ErrorResponseException {
+ ModbusResponse response = send(request);
+ if (response == null)
+ // This should only happen if the request was a broadcast
+ return;
+ if (response.isException())
+ throw new ErrorResponseException(request, response);
+ }
+
+ private void setHoldingRegisterBit(int slaveId, int writeOffset, int bit, boolean value)
+ throws ModbusTransportException, ErrorResponseException {
+ // Writing a bit in a holding register field. There are two ways to do this. The easy way is to
+ // use a write mask request, but it is not always supported. The hard way is to read the value, change
+ // the appropriate bit, and then write it back again (so as not to overwrite the other bits in the
+ // value). However, since the hard way is not atomic, it is not fail-safe either, but it should be
+ // at least possible.
+ SlaveProfile sp = getSlaveProfile(slaveId);
+ if (sp.getWriteMaskRegister()) {
+ // Give the write mask a try.
+ WriteMaskRegisterRequest request = new WriteMaskRegisterRequest(slaveId, writeOffset);
+ request.setBit(bit, value);
+ ModbusResponse response = send(request);
+ if (response == null)
+ // This should only happen if the request was a broadcast
+ return;
+ if (!response.isException())
+ // Hey, cool, it worked.
+ return;
+
+ if (response.getExceptionCode() == ExceptionCode.ILLEGAL_FUNCTION)
+ // The function is probably not supported. Fail-over to the two step.
+ sp.setWriteMaskRegister(false);
+ else
+ throw new ErrorResponseException(request, response);
+ }
+
+ // Do it the hard way. Get the register's current value.
+ int regValue = (Integer) getValue(new NumericLocator(slaveId, RegisterRange.HOLDING_REGISTER, writeOffset,
+ DataType.TWO_BYTE_INT_UNSIGNED));
+
+ // Modify the value according to the given bit and value.
+ if (value)
+ regValue = regValue | 1 << bit;
+ else
+ regValue = regValue & ~(1 << bit);
+
+ // Write the new register value.
+ setValue(new WriteRegisterRequest(slaveId, writeOffset, regValue));
+ }
+
+ private SlaveProfile getSlaveProfile(int slaveId) {
+ SlaveProfile sp = slaveProfiles.get(slaveId);
+ if (sp == null) {
+ sp = new SlaveProfile();
+ slaveProfiles.put(slaveId, sp);
+ }
+ return sp;
+ }
+
+
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ModbusSlaveSet.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ModbusSlaveSet.java
new file mode 100644
index 0000000..6373e5e
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ModbusSlaveSet.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusInitException;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+/**
+ * Abstract ModbusSlaveSet class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+abstract public class ModbusSlaveSet extends Modbus {
+
+ private LinkedHashMap processImages = new LinkedHashMap<>();
+ private ReadWriteLock lock = new ReentrantReadWriteLock();
+
+ /**
+ * addProcessImage.
+ *
+ * @param processImage a {@link ProcessImage} object.
+ */
+ public void addProcessImage(ProcessImage processImage) {
+ lock.writeLock().lock();
+ try {
+ processImages.put(processImage.getSlaveId(), processImage);
+ } finally {
+ lock.writeLock().unlock();
+ }
+ }
+
+ /**
+ * removeProcessImage.
+ *
+ * @param slaveId a int.
+ * @return a boolean.
+ */
+ public boolean removeProcessImage(int slaveId) {
+ lock.writeLock().lock();
+ try {
+ return (processImages.remove(slaveId) != null);
+ } finally {
+ lock.writeLock().unlock();
+ }
+ }
+
+ /**
+ * removeProcessImage.
+ *
+ * @param processImage a {@link ProcessImage} object.
+ * @return a boolean.
+ */
+ public boolean removeProcessImage(ProcessImage processImage) {
+ lock.writeLock().lock();
+ try {
+ return (processImages.remove(processImage.getSlaveId()) != null);
+ } finally {
+ lock.writeLock().unlock();
+ }
+ }
+
+
+ /**
+ * getProcessImage.
+ *
+ * @param slaveId a int.
+ * @return a {@link ProcessImage} object.
+ */
+ public ProcessImage getProcessImage(int slaveId) {
+ lock.readLock().lock();
+ try {
+ return processImages.get(slaveId);
+ } finally {
+ lock.readLock().unlock();
+ }
+ }
+
+ /**
+ * Get a copy of the current process images
+ *
+ * @return a {@link Collection} object.
+ */
+ public Collection getProcessImages() {
+ lock.readLock().lock();
+ try {
+ return new HashSet<>(processImages.values());
+ } finally {
+ lock.readLock().unlock();
+ }
+ }
+
+ /**
+ * Starts the slave. If an exception is not thrown, this method doesn't return, but uses the thread to execute the
+ * listening.
+ *
+ * @throws ModbusInitException if necessary
+ */
+ abstract public void start() throws ModbusInitException;
+
+ /**
+ * stop.
+ */
+ abstract public void stop();
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/NodeScanListener.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/NodeScanListener.java
new file mode 100644
index 0000000..db6c741
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/NodeScanListener.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.ProgressiveTaskListener;
+
+/**
+ * NodeScanListener interface.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public interface NodeScanListener extends ProgressiveTaskListener {
+ /**
+ * nodeFound.
+ *
+ * @param nodeNumber a int.
+ */
+ void nodeFound(int nodeNumber);
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ProcessImage.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ProcessImage.java
new file mode 100644
index 0000000..4500082
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ProcessImage.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.IllegalDataAddressException;
+
+/**
+ * Used by slave implementors. Provides an interface by which slaves can easily manage data.
+ *
+ * @author mlohbihler
+ * @version 2025.9.0
+ */
+public interface ProcessImage {
+ /**
+ * getSlaveId.
+ *
+ * @return a int.
+ */
+ int getSlaveId();
+
+ //
+ // /
+ // / Coils
+ // /
+ //
+
+ /**
+ * Returns the current value of the coil for the given offset.
+ *
+ * @param offset a int.
+ * @return the value of the coil
+ * @throws IllegalDataAddressException if any.
+ */
+ boolean getCoil(int offset) throws IllegalDataAddressException;
+
+ /**
+ * Used internally for setting the value of the coil.
+ *
+ * @param offset a int.
+ * @param value a boolean.
+ */
+ void setCoil(int offset, boolean value);
+
+ /**
+ * Used to set the coil as a result of a write command from the master.
+ *
+ * @param offset a int.
+ * @param value a boolean.
+ * @throws IllegalDataAddressException if any.
+ */
+ void writeCoil(int offset, boolean value) throws IllegalDataAddressException;
+
+ //
+ // /
+ // / Inputs
+ // /
+ //
+
+ /**
+ * Returns the current value of the input for the given offset.
+ *
+ * @param offset a int.
+ * @return the value of the input
+ * @throws IllegalDataAddressException if any.
+ */
+ boolean getInput(int offset) throws IllegalDataAddressException;
+
+ /**
+ * Used internally for setting the value of the input.
+ *
+ * @param offset a int.
+ * @param value a boolean.
+ */
+ void setInput(int offset, boolean value);
+
+ //
+ // /
+ // / Holding registers
+ // /
+ //
+
+ /**
+ * Returns the current value of the holding register for the given offset.
+ *
+ * @param offset a int.
+ * @return the value of the register
+ * @throws IllegalDataAddressException if any.
+ */
+ short getHoldingRegister(int offset) throws IllegalDataAddressException;
+
+ /**
+ * Used internally for setting the value of the holding register.
+ *
+ * @param offset a int.
+ * @param value a short.
+ */
+ void setHoldingRegister(int offset, short value);
+
+ /**
+ * Used to set the holding register as a result of a write command from the master.
+ *
+ * @param offset a int.
+ * @param value a short.
+ * @throws IllegalDataAddressException if any.
+ */
+ void writeHoldingRegister(int offset, short value) throws IllegalDataAddressException;
+
+ //
+ // /
+ // / Input registers
+ // /
+ //
+
+ /**
+ * Returns the current value of the input register for the given offset.
+ *
+ * @param offset a int.
+ * @return the value of the register
+ * @throws IllegalDataAddressException if any.
+ */
+ short getInputRegister(int offset) throws IllegalDataAddressException;
+
+ /**
+ * Used internally for setting the value of the input register.
+ *
+ * @param offset a int.
+ * @param value a short.
+ */
+ void setInputRegister(int offset, short value);
+
+ //
+ // /
+ // / Exception status
+ // /
+ //
+
+ /**
+ * Returns the current value of the exception status.
+ *
+ * @return the current value of the exception status.
+ */
+ byte getExceptionStatus();
+
+ //
+ // /
+ // / Report slave id
+ // /
+ //
+
+ /**
+ * Returns the data for the report slave id command.
+ *
+ * @return the data for the report slave id command.
+ */
+ byte[] getReportSlaveIdData();
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ProcessImageListener.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ProcessImageListener.java
new file mode 100644
index 0000000..4015d0a
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ProcessImageListener.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j;
+
+/**
+ * ProcessImageListener interface.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public interface ProcessImageListener {
+ /**
+ * coilWrite.
+ *
+ * @param offset a int.
+ * @param oldValue a boolean.
+ * @param newValue a boolean.
+ */
+ public void coilWrite(int offset, boolean oldValue, boolean newValue);
+
+ /**
+ * holdingRegisterWrite.
+ *
+ * @param offset a int.
+ * @param oldValue a short.
+ * @param newValue a short.
+ */
+ public void holdingRegisterWrite(int offset, short oldValue, short newValue);
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/base/BaseMessageParser.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/base/BaseMessageParser.java
new file mode 100644
index 0000000..61d4471
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/base/BaseMessageParser.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.IncomingMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.MessageParser;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * Abstract BaseMessageParser class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+abstract public class BaseMessageParser implements MessageParser {
+ protected final boolean master;
+
+ /**
+ * Constructor for BaseMessageParser.
+ *
+ * @param master a boolean.
+ */
+ public BaseMessageParser(boolean master) {
+ this.master = master;
+ }
+
+ @Override
+ public IncomingMessage parseMessage(ByteQueue queue) throws Exception {
+ try {
+ return parseMessageImpl(queue);
+ } catch (ArrayIndexOutOfBoundsException e) {
+ // Means that we ran out of data trying to read the message. Just return null.
+ return null;
+ }
+ }
+
+ /**
+ * parseMessageImpl.
+ *
+ * @param queue a {@link ByteQueue} object.
+ * @return a {@link IncomingMessage} object.
+ * @throws Exception if any.
+ */
+ abstract protected IncomingMessage parseMessageImpl(ByteQueue queue) throws Exception;
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/base/BaseRequestHandler.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/base/BaseRequestHandler.java
new file mode 100644
index 0000000..fc3f6a0
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/base/BaseRequestHandler.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ModbusSlaveSet;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ProcessImage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusRequest;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusResponse;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.RequestHandler;
+
+/**
+ * Abstract BaseRequestHandler class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+abstract public class BaseRequestHandler implements RequestHandler {
+ protected ModbusSlaveSet slave;
+
+ /**
+ * Constructor for BaseRequestHandler.
+ *
+ * @param slave a {@link ModbusSlaveSet} object.
+ */
+ public BaseRequestHandler(ModbusSlaveSet slave) {
+ this.slave = slave;
+ }
+
+ /**
+ * handleRequestImpl.
+ *
+ * @param request a {@link ModbusRequest} object.
+ * @return a {@link ModbusResponse} object.
+ * @throws ModbusTransportException if any.
+ */
+ protected ModbusResponse handleRequestImpl(ModbusRequest request) throws ModbusTransportException {
+ request.validate(slave);
+
+ int slaveId = request.getSlaveId();
+
+ // Check the slave id.
+ if (slaveId == 0) {
+ // Broadcast message. Send to all process images.
+ for (ProcessImage processImage : slave.getProcessImages())
+ request.handle(processImage);
+ return null;
+ }
+
+ // Find the process image to which to send.
+ ProcessImage processImage = slave.getProcessImage(slaveId);
+ if (processImage == null)
+ return null;
+
+ return request.handle(processImage);
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/base/KeyedModbusLocator.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/base/KeyedModbusLocator.java
new file mode 100644
index 0000000..ea3b40f
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/base/KeyedModbusLocator.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ExceptionResult;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.ExceptionCode;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.locator.BaseLocator;
+
+/**
+ * KeyedModbusLocator class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class KeyedModbusLocator {
+ private final K key;
+ private final BaseLocator> locator;
+
+ /**
+ * Constructor for KeyedModbusLocator.
+ *
+ * @param key a K object.
+ * @param locator a {@link BaseLocator} object.
+ */
+ public KeyedModbusLocator(K key, BaseLocator> locator) {
+ this.key = key;
+ this.locator = locator;
+ }
+
+ /**
+ * Getter for the field key.
+ *
+ * @return a K object.
+ */
+ public K getKey() {
+ return key;
+ }
+
+ /**
+ * Getter for the field locator.
+ *
+ * @return a {@link BaseLocator} object.
+ */
+ public BaseLocator> getLocator() {
+ return locator;
+ }
+
+ @Override
+ public String toString() {
+ return "KeyedModbusLocator(key=" + key + ", locator=" + locator + ")";
+ }
+
+ //
+ ///
+ /// Delegation.
+ ///
+ //
+
+ /**
+ * getDataType.
+ *
+ * @return a int.
+ */
+ public int getDataType() {
+ return locator.getDataType();
+ }
+
+ /**
+ * getOffset.
+ *
+ * @return a int.
+ */
+ public int getOffset() {
+ return locator.getOffset();
+ }
+
+ /**
+ * getSlaveAndRange.
+ *
+ * @return a {@link SlaveAndRange} object.
+ */
+ public SlaveAndRange getSlaveAndRange() {
+ return new SlaveAndRange(locator.getSlaveId(), locator.getRange());
+ }
+
+ /**
+ * getEndOffset.
+ *
+ * @return a int.
+ */
+ public int getEndOffset() {
+ return locator.getEndOffset();
+ }
+
+ /**
+ * getRegisterCount.
+ *
+ * @return a int.
+ */
+ public int getRegisterCount() {
+ return locator.getRegisterCount();
+ }
+
+ /**
+ * bytesToValue.
+ *
+ * @param data an array of {@link byte} objects.
+ * @param requestOffset a int.
+ * @return a {@link Object} object.
+ */
+ public Object bytesToValue(byte[] data, int requestOffset) {
+ try {
+ return locator.bytesToValue(data, requestOffset);
+ } catch (ArrayIndexOutOfBoundsException e) {
+ // Some equipment will not return data lengths that we expect, which causes AIOOBEs. Catch them and convert
+ // them into illegal data address exceptions.
+ return new ExceptionResult(ExceptionCode.ILLEGAL_DATA_ADDRESS);
+ }
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/base/ModbusUtils.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/base/ModbusUtils.java
new file mode 100644
index 0000000..4181262
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/base/ModbusUtils.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.RegisterRange;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.IllegalSlaveIdException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusIdException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * ModbusUtils class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class ModbusUtils {
+ /**
+ * Constant TCP_PORT=502
+ */
+ public static final int TCP_PORT = 502;
+ /**
+ * Constant IP_PROTOCOL_ID=0
+ */
+ public static final int IP_PROTOCOL_ID = 0; // Modbus protocol
+
+ // public static final int MAX_READ_BIT_COUNT = 2000;
+ // public static final int MAX_READ_REGISTER_COUNT = 125;
+ // public static final int MAX_WRITE_REGISTER_COUNT = 120;
+ // Table of CRC values for high-order byte
+ private final static short[] lookupCRCHi = {0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80,
+ 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01,
+ 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80,
+ 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01,
+ 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
+ 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01,
+ 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81,
+ 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00,
+ 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80,
+ 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00,
+ 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80,
+ 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01,
+ 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80,
+ 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01,
+ 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40};
+ // Table of CRC values for low-order byte
+ private final static short[] lookupCRCLo = {0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 0x07,
+ 0xC7, 0x05, 0xC5, 0xC4, 0x04, 0xCC, 0x0C, 0x0D, 0xCD, 0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9,
+ 0x09, 0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A, 0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C,
+ 0xDC, 0x14, 0xD4, 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3, 0x11, 0xD1, 0xD0, 0x10, 0xF0,
+ 0x30, 0x31, 0xF1, 0x33, 0xF3, 0xF2, 0x32, 0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4, 0x3C, 0xFC, 0xFD,
+ 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A, 0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29, 0xEB,
+ 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF, 0x2D, 0xED, 0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6,
+ 0x26, 0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60, 0x61, 0xA1, 0x63, 0xA3, 0xA2, 0x62, 0x66,
+ 0xA6, 0xA7, 0x67, 0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F, 0x6E, 0xAE, 0xAA, 0x6A, 0x6B,
+ 0xAB, 0x69, 0xA9, 0xA8, 0x68, 0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA, 0xBE, 0x7E, 0x7F, 0xBF, 0x7D,
+ 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5, 0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 0x70,
+ 0xB0, 0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C,
+ 0x5C, 0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B, 0x99, 0x59, 0x58, 0x98, 0x88, 0x48, 0x49,
+ 0x89, 0x4B, 0x8B, 0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C, 0x44, 0x84, 0x85, 0x45, 0x87,
+ 0x47, 0x46, 0x86, 0x82, 0x42, 0x43, 0x83, 0x41, 0x81, 0x80, 0x40};
+
+ /**
+ * pushByte.
+ *
+ * @param queue a {@link ByteQueue} object.
+ * @param value a int.
+ */
+ public static void pushByte(ByteQueue queue, int value) {
+ queue.push((byte) value);
+ }
+
+ /**
+ * pushShort.
+ *
+ * @param queue a {@link ByteQueue} object.
+ * @param value a int.
+ */
+ public static void pushShort(ByteQueue queue, int value) {
+ queue.push((byte) (0xff & (value >> 8)));
+ queue.push((byte) (0xff & value));
+ }
+
+ /**
+ * popByte.
+ *
+ * @param queue a {@link ByteQueue} object.
+ * @return a int.
+ */
+ public static int popByte(ByteQueue queue) {
+ return queue.pop();
+ }
+
+ /**
+ * popUnsignedByte.
+ *
+ * @param queue a {@link ByteQueue} object.
+ * @return a int.
+ */
+ public static int popUnsignedByte(ByteQueue queue) {
+ return queue.pop() & 0xff;
+ }
+
+ /**
+ * popShort.
+ *
+ * @param queue a {@link ByteQueue} object.
+ * @return a int.
+ */
+ public static int popShort(ByteQueue queue) {
+ return toShort(queue.pop(), queue.pop());
+ }
+
+ /**
+ * popUnsignedShort.
+ *
+ * @param queue a {@link ByteQueue} object.
+ * @return a int.
+ */
+ public static int popUnsignedShort(ByteQueue queue) {
+ return ((queue.pop() & 0xff) << 8) | (queue.pop() & 0xff);
+ }
+
+ /**
+ * toShort.
+ *
+ * @param b1 a byte.
+ * @param b2 a byte.
+ * @return a short.
+ */
+ public static short toShort(byte b1, byte b2) {
+ return (short) ((b1 << 8) | (b2 & 0xff));
+ }
+
+ /**
+ * toByte.
+ *
+ * @param value a short.
+ * @param first a boolean.
+ * @return a byte.
+ */
+ public static byte toByte(short value, boolean first) {
+ if (first)
+ return (byte) (0xff & (value >> 8));
+ return (byte) (0xff & value);
+ }
+
+ /**
+ * validateRegisterRange.
+ *
+ * @param range a int.
+ */
+ public static void validateRegisterRange(int range) {
+ if (RegisterRange.getReadFunctionCode(range) == -1)
+ throw new ModbusIdException("Invalid register range: " + range);
+ }
+
+ /**
+ * validateSlaveId.
+ *
+ * @param slaveId a int.
+ * @param includeBroadcast a boolean.
+ */
+ public static void validateSlaveId(int slaveId, boolean includeBroadcast) {
+ if (slaveId < (includeBroadcast ? 0 : 1) /* || slaveId > 240 */)
+ throw new IllegalSlaveIdException("Invalid slave id: " + slaveId);
+ }
+
+ /**
+ * validateBit.
+ *
+ * @param bit a int.
+ */
+ public static void validateBit(int bit) {
+ if (bit < 0 || bit > 15)
+ throw new ModbusIdException("Invalid bit: " + bit);
+ }
+
+ /**
+ * validateOffset.
+ *
+ * @param offset a int.
+ * @throws ModbusTransportException if any.
+ */
+ public static void validateOffset(int offset) throws ModbusTransportException {
+ if (offset < 0 || offset > 65535)
+ throw new ModbusTransportException("Invalid offset: " + offset);
+ }
+
+ /**
+ * validateEndOffset.
+ *
+ * @param offset a int.
+ * @throws ModbusTransportException if any.
+ */
+ public static void validateEndOffset(int offset) throws ModbusTransportException {
+ if (offset > 65535)
+ throw new ModbusTransportException("Invalid end offset: " + offset);
+ }
+
+ /**
+ * checkCRC.
+ *
+ * @param modbusMessage a {@link ModbusMessage} object.
+ * @param queue a {@link ByteQueue} object.
+ * @throws ModbusTransportException if any.
+ */
+ public static void checkCRC(ModbusMessage modbusMessage, ByteQueue queue) throws ModbusTransportException {
+ // Check the CRC
+ int calcCrc = calculateCRC(modbusMessage);
+ int givenCrc = ModbusUtils.popUnsignedShort(queue);
+
+ if (calcCrc != givenCrc)
+ throw new ModbusTransportException("CRC mismatch: given=" + givenCrc + ", calc=" + calcCrc,
+ modbusMessage.getSlaveId());
+ }
+
+ /**
+ * calculateCRC.
+ *
+ * @param modbusMessage a {@link ModbusMessage} object.
+ * @return a int.
+ */
+ public static int calculateCRC(ModbusMessage modbusMessage) {
+ ByteQueue queue = new ByteQueue();
+ modbusMessage.write(queue);
+
+ int high = 0xff;
+ int low = 0xff;
+ int nextByte = 0;
+ int uIndex;
+
+ while (queue.size() > 0) {
+ nextByte = 0xFF & queue.pop();
+ uIndex = high ^ nextByte;
+ high = low ^ lookupCRCHi[uIndex];
+ low = lookupCRCLo[uIndex];
+ }
+
+ return (high << 8) | low;
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/base/RangeAndOffset.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/base/RangeAndOffset.java
new file mode 100644
index 0000000..e6bacd5
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/base/RangeAndOffset.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.RegisterRange;
+
+/**
+ * RangeAndOffset class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class RangeAndOffset {
+ private int range;
+ private int offset;
+
+ /**
+ * Constructor for RangeAndOffset.
+ *
+ * @param range a int.
+ * @param offset a int.
+ */
+ public RangeAndOffset(int range, int offset) {
+ this.range = range;
+ this.offset = offset;
+ }
+
+ /**
+ * This constructor provides a best guess at the function and offset the user wants, with the assumption that the
+ * offset will never go over 9999.
+ *
+ * @param registerId a int.
+ */
+ public RangeAndOffset(int registerId) {
+ if (registerId < 10000) {
+ this.range = RegisterRange.COIL_STATUS;
+ this.offset = registerId - 1;
+ } else if (registerId < 20000) {
+ this.range = RegisterRange.INPUT_STATUS;
+ this.offset = registerId - 10001;
+ } else if (registerId < 40000) {
+ this.range = RegisterRange.INPUT_REGISTER;
+ this.offset = registerId - 30001;
+ } else {
+ this.range = RegisterRange.HOLDING_REGISTER;
+ this.offset = registerId - 40001;
+ }
+ }
+
+ /**
+ * Getter for the field range.
+ *
+ * @return a int.
+ */
+ public int getRange() {
+ return range;
+ }
+
+ /**
+ * Getter for the field offset.
+ *
+ * @return a int.
+ */
+ public int getOffset() {
+ return offset;
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/base/ReadFunctionGroup.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/base/ReadFunctionGroup.java
new file mode 100644
index 0000000..3e6e034
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/base/ReadFunctionGroup.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.RegisterRange;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * ReadFunctionGroup class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class ReadFunctionGroup {
+ private final SlaveAndRange slaveAndRange;
+ private final int functionCode;
+ private final List> locators = new ArrayList<>();
+ private int startOffset = 65536;
+ private int length = 0;
+
+ /**
+ * Constructor for ReadFunctionGroup.
+ *
+ * @param locator a {@link KeyedModbusLocator} object.
+ */
+ public ReadFunctionGroup(KeyedModbusLocator locator) {
+ slaveAndRange = locator.getSlaveAndRange();
+ functionCode = RegisterRange.getReadFunctionCode(slaveAndRange.getRange());
+ add(locator);
+ }
+
+ /**
+ * add.
+ *
+ * @param locator a {@link KeyedModbusLocator} object.
+ */
+ public void add(KeyedModbusLocator locator) {
+ if (startOffset > locator.getOffset())
+ startOffset = locator.getOffset();
+ if (length < locator.getEndOffset() - startOffset + 1)
+ length = locator.getEndOffset() - startOffset + 1;
+ locators.add(locator);
+ }
+
+ /**
+ * Getter for the field startOffset.
+ *
+ * @return a int.
+ */
+ public int getStartOffset() {
+ return startOffset;
+ }
+
+ /**
+ * getEndOffset.
+ *
+ * @return a int.
+ */
+ public int getEndOffset() {
+ return startOffset + length - 1;
+ }
+
+ /**
+ * Getter for the field slaveAndRange.
+ *
+ * @return a {@link SlaveAndRange} object.
+ */
+ public SlaveAndRange getSlaveAndRange() {
+ return slaveAndRange;
+ }
+
+ /**
+ * Getter for the field length.
+ *
+ * @return a int.
+ */
+ public int getLength() {
+ return length;
+ }
+
+ /**
+ * Getter for the field functionCode.
+ *
+ * @return a int.
+ */
+ public int getFunctionCode() {
+ return functionCode;
+ }
+
+ /**
+ * Getter for the field locators.
+ *
+ * @return a {@link List} object.
+ */
+ public List> getLocators() {
+ return locators;
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/base/SlaveAndRange.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/base/SlaveAndRange.java
new file mode 100644
index 0000000..2512008
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/base/SlaveAndRange.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base;
+
+/**
+ * SlaveAndRange class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class SlaveAndRange {
+ private final int slaveId;
+ private final int range;
+
+ /**
+ * Constructor for SlaveAndRange.
+ *
+ * @param slaveId a int.
+ * @param range a int.
+ */
+ public SlaveAndRange(int slaveId, int range) {
+ ModbusUtils.validateSlaveId(slaveId, true);
+
+ this.slaveId = slaveId;
+ this.range = range;
+ }
+
+ /**
+ * Getter for the field range.
+ *
+ * @return a int.
+ */
+ public int getRange() {
+ return range;
+ }
+
+ /**
+ * Getter for the field slaveId.
+ *
+ * @return a int.
+ */
+ public int getSlaveId() {
+ return slaveId;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + range;
+ result = prime * result + slaveId;
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ final SlaveAndRange other = (SlaveAndRange) obj;
+ if (range != other.range)
+ return false;
+ if (slaveId != other.slaveId)
+ return false;
+ return true;
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/base/SlaveProfile.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/base/SlaveProfile.java
new file mode 100644
index 0000000..5e1af1b
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/base/SlaveProfile.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base;
+
+/**
+ * Class for maintaining the profile of a slave device on the master side. Initially, we assume that the device is fully
+ * featured, and then we note function failures so that we know how requests should subsequently be sent.
+ *
+ * @author mlohbihler
+ * @version 2025.9.0
+ */
+public class SlaveProfile {
+ private boolean writeMaskRegister = true;
+
+ /**
+ * Getter for the field writeMaskRegister.
+ *
+ * @return a boolean.
+ */
+ public boolean getWriteMaskRegister() {
+ return writeMaskRegister;
+ }
+
+ /**
+ * Setter for the field writeMaskRegister.
+ *
+ * @param writeMaskRegister a boolean.
+ */
+ public void setWriteMaskRegister(boolean writeMaskRegister) {
+ this.writeMaskRegister = writeMaskRegister;
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/code/DataType.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/code/DataType.java
new file mode 100644
index 0000000..7195e6e
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/code/DataType.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code;
+
+import java.math.BigInteger;
+
+/**
+ * DataType class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class DataType {
+ /**
+ * Constant BINARY=1
+ */
+ public static final int BINARY = 1;
+
+ /**
+ * Constant TWO_BYTE_INT_UNSIGNED=2
+ */
+ public static final int TWO_BYTE_INT_UNSIGNED = 2;
+ /**
+ * Constant TWO_BYTE_INT_SIGNED=3
+ */
+ public static final int TWO_BYTE_INT_SIGNED = 3;
+ /**
+ * Constant TWO_BYTE_INT_UNSIGNED_SWAPPED=22
+ */
+ public static final int TWO_BYTE_INT_UNSIGNED_SWAPPED = 22;
+ /**
+ * Constant TWO_BYTE_INT_SIGNED_SWAPPED=23
+ */
+ public static final int TWO_BYTE_INT_SIGNED_SWAPPED = 23;
+
+ /**
+ * Constant FOUR_BYTE_INT_UNSIGNED=4
+ */
+ public static final int FOUR_BYTE_INT_UNSIGNED = 4;
+ /**
+ * Constant FOUR_BYTE_INT_SIGNED=5
+ */
+ public static final int FOUR_BYTE_INT_SIGNED = 5;
+ /**
+ * Constant FOUR_BYTE_INT_UNSIGNED_SWAPPED=6
+ */
+ public static final int FOUR_BYTE_INT_UNSIGNED_SWAPPED = 6;
+ /**
+ * Constant FOUR_BYTE_INT_SIGNED_SWAPPED=7
+ */
+ public static final int FOUR_BYTE_INT_SIGNED_SWAPPED = 7;
+ /* 0xAABBCCDD is transmitted as 0xDDCCBBAA */
+ /**
+ * Constant FOUR_BYTE_INT_UNSIGNED_SWAPPED_SWAPPED=24
+ */
+ public static final int FOUR_BYTE_INT_UNSIGNED_SWAPPED_SWAPPED = 24;
+ /**
+ * Constant FOUR_BYTE_INT_SIGNED_SWAPPED_SWAPPED=25
+ */
+ public static final int FOUR_BYTE_INT_SIGNED_SWAPPED_SWAPPED = 25;
+
+ /**
+ * Constant FOUR_BYTE_FLOAT=8
+ */
+ public static final int FOUR_BYTE_FLOAT = 8;
+ /**
+ * Constant FOUR_BYTE_FLOAT_SWAPPED=9
+ */
+ public static final int FOUR_BYTE_FLOAT_SWAPPED = 9;
+ /**
+ * Constant FOUR_BYTE_FLOAT_SWAPPED_INVERTED=21
+ */
+ public static final int FOUR_BYTE_FLOAT_SWAPPED_INVERTED = 21;
+
+ /**
+ * Constant EIGHT_BYTE_INT_UNSIGNED=10
+ */
+ public static final int EIGHT_BYTE_INT_UNSIGNED = 10;
+ /**
+ * Constant EIGHT_BYTE_INT_SIGNED=11
+ */
+ public static final int EIGHT_BYTE_INT_SIGNED = 11;
+ /**
+ * Constant EIGHT_BYTE_INT_UNSIGNED_SWAPPED=12
+ */
+ public static final int EIGHT_BYTE_INT_UNSIGNED_SWAPPED = 12;
+ /**
+ * Constant EIGHT_BYTE_INT_SIGNED_SWAPPED=13
+ */
+ public static final int EIGHT_BYTE_INT_SIGNED_SWAPPED = 13;
+ /**
+ * Constant EIGHT_BYTE_FLOAT=14
+ */
+ public static final int EIGHT_BYTE_FLOAT = 14;
+ /**
+ * Constant EIGHT_BYTE_FLOAT_SWAPPED=15
+ */
+ public static final int EIGHT_BYTE_FLOAT_SWAPPED = 15;
+
+ /**
+ * Constant TWO_BYTE_BCD=16
+ */
+ public static final int TWO_BYTE_BCD = 16;
+ /**
+ * Constant FOUR_BYTE_BCD=17
+ */
+ public static final int FOUR_BYTE_BCD = 17;
+ /**
+ * Constant FOUR_BYTE_BCD_SWAPPED=20
+ */
+ public static final int FOUR_BYTE_BCD_SWAPPED = 20;
+
+ /**
+ * Constant CHAR=18
+ */
+ public static final int CHAR = 18;
+ /**
+ * Constant VARCHAR=19
+ */
+ public static final int VARCHAR = 19;
+
+ //MOD10K two, three and four register types
+ /**
+ * Constant FOUR_BYTE_MOD_10K=26
+ */
+ public static final int FOUR_BYTE_MOD_10K = 26;
+ /**
+ * Constant SIX_BYTE_MOD_10K=27
+ */
+ public static final int SIX_BYTE_MOD_10K = 27;
+ /**
+ * Constant EIGHT_BYTE_MOD_10K=28
+ */
+ public static final int EIGHT_BYTE_MOD_10K = 28;
+ /**
+ * Constant FOUR_BYTE_MOD_10K_SWAPPED=29
+ */
+ public static final int FOUR_BYTE_MOD_10K_SWAPPED = 29;
+ /**
+ * Constant SIX_BYTE_MOD_10K_SWAPPED=30
+ */
+ public static final int SIX_BYTE_MOD_10K_SWAPPED = 30;
+ /**
+ * Constant EIGHT_BYTE_MOD_10K_SWAPPED=31
+ */
+ public static final int EIGHT_BYTE_MOD_10K_SWAPPED = 31;
+
+ //One byte unsigned integer types
+ /**
+ * Constant ONE_BYTE_INT_UNSIGNED_LOWER=32
+ */
+ public static final int ONE_BYTE_INT_UNSIGNED_LOWER = 32;
+ /**
+ * Constant ONE_BYTE_INT_UNSIGNED_UPPER=33
+ */
+ public static final int ONE_BYTE_INT_UNSIGNED_UPPER = 33;
+
+ /**
+ * getRegisterCount.
+ *
+ * @param id a int.
+ * @return a int.
+ */
+ public static int getRegisterCount(int id) {
+ switch (id) {
+ case BINARY:
+ case TWO_BYTE_INT_UNSIGNED:
+ case TWO_BYTE_INT_SIGNED:
+ case TWO_BYTE_INT_UNSIGNED_SWAPPED:
+ case TWO_BYTE_INT_SIGNED_SWAPPED:
+ case TWO_BYTE_BCD:
+ case ONE_BYTE_INT_UNSIGNED_LOWER:
+ case ONE_BYTE_INT_UNSIGNED_UPPER:
+ return 1;
+ case FOUR_BYTE_INT_UNSIGNED:
+ case FOUR_BYTE_INT_SIGNED:
+ case FOUR_BYTE_INT_UNSIGNED_SWAPPED:
+ case FOUR_BYTE_INT_SIGNED_SWAPPED:
+ case FOUR_BYTE_INT_UNSIGNED_SWAPPED_SWAPPED:
+ case FOUR_BYTE_INT_SIGNED_SWAPPED_SWAPPED:
+ case FOUR_BYTE_FLOAT:
+ case FOUR_BYTE_FLOAT_SWAPPED:
+ case FOUR_BYTE_FLOAT_SWAPPED_INVERTED:
+ case FOUR_BYTE_BCD:
+ case FOUR_BYTE_BCD_SWAPPED:
+ case FOUR_BYTE_MOD_10K:
+ case FOUR_BYTE_MOD_10K_SWAPPED:
+ return 2;
+ case SIX_BYTE_MOD_10K:
+ case SIX_BYTE_MOD_10K_SWAPPED:
+ return 3;
+ case EIGHT_BYTE_INT_UNSIGNED:
+ case EIGHT_BYTE_INT_SIGNED:
+ case EIGHT_BYTE_INT_UNSIGNED_SWAPPED:
+ case EIGHT_BYTE_INT_SIGNED_SWAPPED:
+ case EIGHT_BYTE_FLOAT:
+ case EIGHT_BYTE_FLOAT_SWAPPED:
+ case EIGHT_BYTE_MOD_10K:
+ case EIGHT_BYTE_MOD_10K_SWAPPED:
+ return 4;
+ }
+ return 0;
+ }
+
+ /**
+ * getJavaType.
+ *
+ * @param id a int.
+ * @return a {@link Class} object.
+ */
+ public static Class> getJavaType(int id) {
+ switch (id) {
+ case ONE_BYTE_INT_UNSIGNED_LOWER:
+ case ONE_BYTE_INT_UNSIGNED_UPPER:
+ return Integer.class;
+ case BINARY:
+ return Boolean.class;
+ case TWO_BYTE_INT_UNSIGNED:
+ case TWO_BYTE_INT_UNSIGNED_SWAPPED:
+ return Integer.class;
+ case TWO_BYTE_INT_SIGNED:
+ case TWO_BYTE_INT_SIGNED_SWAPPED:
+ return Short.class;
+ case FOUR_BYTE_INT_UNSIGNED:
+ return Long.class;
+ case FOUR_BYTE_INT_SIGNED:
+ return Integer.class;
+ case FOUR_BYTE_INT_UNSIGNED_SWAPPED:
+ case FOUR_BYTE_INT_UNSIGNED_SWAPPED_SWAPPED:
+ return Long.class;
+ case FOUR_BYTE_INT_SIGNED_SWAPPED:
+ case FOUR_BYTE_INT_SIGNED_SWAPPED_SWAPPED:
+ return Integer.class;
+ case FOUR_BYTE_FLOAT:
+ return Float.class;
+ case FOUR_BYTE_FLOAT_SWAPPED:
+ return Float.class;
+ case FOUR_BYTE_FLOAT_SWAPPED_INVERTED:
+ return Float.class;
+ case EIGHT_BYTE_INT_UNSIGNED:
+ return BigInteger.class;
+ case EIGHT_BYTE_INT_SIGNED:
+ return Long.class;
+ case EIGHT_BYTE_INT_UNSIGNED_SWAPPED:
+ return BigInteger.class;
+ case EIGHT_BYTE_INT_SIGNED_SWAPPED:
+ return Long.class;
+ case EIGHT_BYTE_FLOAT:
+ return Double.class;
+ case EIGHT_BYTE_FLOAT_SWAPPED:
+ return Double.class;
+ case TWO_BYTE_BCD:
+ return Short.class;
+ case FOUR_BYTE_BCD:
+ case FOUR_BYTE_BCD_SWAPPED:
+ return Integer.class;
+ case CHAR:
+ case VARCHAR:
+ return String.class;
+ case FOUR_BYTE_MOD_10K:
+ case SIX_BYTE_MOD_10K:
+ case EIGHT_BYTE_MOD_10K:
+ case FOUR_BYTE_MOD_10K_SWAPPED:
+ case SIX_BYTE_MOD_10K_SWAPPED:
+ case EIGHT_BYTE_MOD_10K_SWAPPED:
+ return BigInteger.class;
+ }
+ return null;
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/code/ExceptionCode.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/code/ExceptionCode.java
new file mode 100644
index 0000000..1857f57
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/code/ExceptionCode.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code;
+
+/**
+ * ExceptionCode class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class ExceptionCode {
+ /**
+ * Constant ILLEGAL_FUNCTION=0x1
+ */
+ public static final byte ILLEGAL_FUNCTION = 0x1;
+ /**
+ * Constant ILLEGAL_DATA_ADDRESS=0x2
+ */
+ public static final byte ILLEGAL_DATA_ADDRESS = 0x2;
+ /**
+ * Constant ILLEGAL_DATA_VALUE=0x3
+ */
+ public static final byte ILLEGAL_DATA_VALUE = 0x3;
+ /**
+ * Constant SLAVE_DEVICE_FAILURE=0x4
+ */
+ public static final byte SLAVE_DEVICE_FAILURE = 0x4;
+ /**
+ * Constant ACKNOWLEDGE=0x5
+ */
+ public static final byte ACKNOWLEDGE = 0x5;
+ /**
+ * Constant SLAVE_DEVICE_BUSY=0x6
+ */
+ public static final byte SLAVE_DEVICE_BUSY = 0x6;
+ /**
+ * Constant MEMORY_PARITY_ERROR=0x8
+ */
+ public static final byte MEMORY_PARITY_ERROR = 0x8;
+ /**
+ * Constant GATEWAY_PATH_UNAVAILABLE=0xa
+ */
+ public static final byte GATEWAY_PATH_UNAVAILABLE = 0xa;
+ /**
+ * Constant GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND=0xb
+ */
+ public static final byte GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND = 0xb;
+
+ /**
+ * getExceptionMessage.
+ *
+ * @param id a byte.
+ * @return a {@link String} object.
+ */
+ public static String getExceptionMessage(byte id) {
+ switch (id) {
+ case ILLEGAL_FUNCTION:
+ return "Illegal function";
+ case ILLEGAL_DATA_ADDRESS:
+ return "Illegal data address";
+ case ILLEGAL_DATA_VALUE:
+ return "Illegal data value";
+ case SLAVE_DEVICE_FAILURE:
+ return "Slave device failure";
+ case ACKNOWLEDGE:
+ return "Acknowledge";
+ case SLAVE_DEVICE_BUSY:
+ return "Slave device busy";
+ case MEMORY_PARITY_ERROR:
+ return "Memory parity error";
+ case GATEWAY_PATH_UNAVAILABLE:
+ return "Gateway path unavailable";
+ case GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND:
+ return "Gateway target device failed to respond";
+ }
+ return "Unknown exception code: " + id;
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/code/FunctionCode.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/code/FunctionCode.java
new file mode 100644
index 0000000..094abca
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/code/FunctionCode.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code;
+
+/**
+ * FunctionCode class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class FunctionCode {
+ /**
+ * Constant READ_COILS=1
+ */
+ public static final byte READ_COILS = 1;
+ /**
+ * Constant READ_DISCRETE_INPUTS=2
+ */
+ public static final byte READ_DISCRETE_INPUTS = 2;
+ /**
+ * Constant READ_HOLDING_REGISTERS=3
+ */
+ public static final byte READ_HOLDING_REGISTERS = 3;
+ /**
+ * Constant READ_INPUT_REGISTERS=4
+ */
+ public static final byte READ_INPUT_REGISTERS = 4;
+ /**
+ * Constant WRITE_COIL=5
+ */
+ public static final byte WRITE_COIL = 5;
+ /**
+ * Constant WRITE_REGISTER=6
+ */
+ public static final byte WRITE_REGISTER = 6;
+ /**
+ * Constant READ_EXCEPTION_STATUS=7
+ */
+ public static final byte READ_EXCEPTION_STATUS = 7;
+ /**
+ * Constant WRITE_COILS=15
+ */
+ public static final byte WRITE_COILS = 15;
+ /**
+ * Constant WRITE_REGISTERS=16
+ */
+ public static final byte WRITE_REGISTERS = 16;
+ /**
+ * Constant REPORT_SLAVE_ID=17
+ */
+ public static final byte REPORT_SLAVE_ID = 17;
+ /**
+ * Constant WRITE_MASK_REGISTER=22
+ */
+ public static final byte WRITE_MASK_REGISTER = 22;
+
+ /**
+ * toString.
+ *
+ * @param code a byte.
+ * @return a {@link String} object.
+ */
+ public static String toString(byte code) {
+ return Integer.toString(code & 0xff);
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/code/RegisterRange.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/code/RegisterRange.java
new file mode 100644
index 0000000..a504582
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/code/RegisterRange.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code;
+
+/**
+ * RegisterRange class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class RegisterRange {
+ /**
+ * Constant COIL_STATUS=1
+ */
+ public static final int COIL_STATUS = 1;
+ /**
+ * Constant INPUT_STATUS=2
+ */
+ public static final int INPUT_STATUS = 2;
+ /**
+ * Constant HOLDING_REGISTER=3
+ */
+ public static final int HOLDING_REGISTER = 3;
+ /**
+ * Constant INPUT_REGISTER=4
+ */
+ public static final int INPUT_REGISTER = 4;
+
+ /**
+ * getFrom.
+ *
+ * @param id a int.
+ * @return a int.
+ */
+ public static int getFrom(int id) {
+ switch (id) {
+ case COIL_STATUS:
+ return 0;
+ case INPUT_STATUS:
+ return 0x10000;
+ case HOLDING_REGISTER:
+ return 0x40000;
+ case INPUT_REGISTER:
+ return 0x30000;
+ }
+ return -1;
+ }
+
+ /**
+ * getTo.
+ *
+ * @param id a int.
+ * @return a int.
+ */
+ public static int getTo(int id) {
+ switch (id) {
+ case COIL_STATUS:
+ return 0xffff;
+ case INPUT_STATUS:
+ return 0x1ffff;
+ case HOLDING_REGISTER:
+ return 0x4ffff;
+ case INPUT_REGISTER:
+ return 0x3ffff;
+ }
+ return -1;
+ }
+
+ /**
+ * getReadFunctionCode.
+ *
+ * @param id a int.
+ * @return a int.
+ */
+ public static int getReadFunctionCode(int id) {
+ switch (id) {
+ case COIL_STATUS:
+ return FunctionCode.READ_COILS;
+ case INPUT_STATUS:
+ return FunctionCode.READ_DISCRETE_INPUTS;
+ case HOLDING_REGISTER:
+ return FunctionCode.READ_HOLDING_REGISTERS;
+ case INPUT_REGISTER:
+ return FunctionCode.READ_INPUT_REGISTERS;
+ }
+ return -1;
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/exception/ErrorResponseException.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/exception/ErrorResponseException.java
new file mode 100644
index 0000000..2ec9687
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/exception/ErrorResponseException.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusRequest;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusResponse;
+
+/**
+ * ErrorResponseException class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class ErrorResponseException extends Exception {
+ private static final long serialVersionUID = -1;
+
+ private final ModbusRequest originalRequest;
+ private final ModbusResponse errorResponse;
+
+ /**
+ * Constructor for ErrorResponseException.
+ *
+ * @param originalRequest a {@link ModbusRequest} object.
+ * @param errorResponse a {@link ModbusResponse} object.
+ */
+ public ErrorResponseException(ModbusRequest originalRequest, ModbusResponse errorResponse) {
+ this.originalRequest = originalRequest;
+ this.errorResponse = errorResponse;
+ }
+
+ /**
+ * Getter for the field errorResponse.
+ *
+ * @return a {@link ModbusResponse} object.
+ */
+ public ModbusResponse getErrorResponse() {
+ return errorResponse;
+ }
+
+ /**
+ * Getter for the field originalRequest.
+ *
+ * @return a {@link ModbusRequest} object.
+ */
+ public ModbusRequest getOriginalRequest() {
+ return originalRequest;
+ }
+
+ @Override
+ public String getMessage() {
+ return errorResponse.getExceptionMessage();
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/exception/IllegalDataAddressException.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/exception/IllegalDataAddressException.java
new file mode 100644
index 0000000..c6b5950
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/exception/IllegalDataAddressException.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception;
+
+/**
+ * IllegalDataAddressException class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class IllegalDataAddressException extends ModbusTransportException {
+ private static final long serialVersionUID = -1;
+
+ /**
+ * Constructor for IllegalDataAddressException.
+ */
+ public IllegalDataAddressException() {
+ super();
+ }
+
+ /**
+ * Constructor for IllegalDataAddressException.
+ *
+ * @param slaveId a int.
+ */
+ public IllegalDataAddressException(int slaveId) {
+ super(slaveId);
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/exception/IllegalDataTypeException.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/exception/IllegalDataTypeException.java
new file mode 100644
index 0000000..43f3793
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/exception/IllegalDataTypeException.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception;
+
+/**
+ * IllegalDataTypeException class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class IllegalDataTypeException extends ModbusIdException {
+ private static final long serialVersionUID = -1;
+
+ /**
+ * Constructor for IllegalDataTypeException.
+ *
+ * @param message a {@link String} object.
+ */
+ public IllegalDataTypeException(String message) {
+ super(message);
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/exception/IllegalFunctionException.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/exception/IllegalFunctionException.java
new file mode 100644
index 0000000..ed9df81
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/exception/IllegalFunctionException.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception;
+
+/**
+ * IllegalFunctionException class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class IllegalFunctionException extends ModbusTransportException {
+ private static final long serialVersionUID = -1;
+
+ private final byte functionCode;
+
+ /**
+ * Constructor for IllegalFunctionException.
+ *
+ * @param functionCode a byte.
+ * @param slaveId a int.
+ */
+ public IllegalFunctionException(byte functionCode, int slaveId) {
+ super("Function code: 0x" + Integer.toHexString(functionCode & 0xff), slaveId);
+ this.functionCode = functionCode;
+ }
+
+ /**
+ * Getter for the field functionCode.
+ *
+ * @return a byte.
+ */
+ public byte getFunctionCode() {
+ return functionCode;
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/exception/IllegalSlaveIdException.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/exception/IllegalSlaveIdException.java
new file mode 100644
index 0000000..cb4bd7b
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/exception/IllegalSlaveIdException.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception;
+
+/**
+ * IllegalSlaveIdException class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class IllegalSlaveIdException extends ModbusIdException {
+ private static final long serialVersionUID = -1;
+
+ /**
+ * Constructor for IllegalSlaveIdException.
+ *
+ * @param message a {@link String} object.
+ */
+ public IllegalSlaveIdException(String message) {
+ super(message);
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/exception/InvalidDataConversionException.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/exception/InvalidDataConversionException.java
new file mode 100644
index 0000000..09a2891
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/exception/InvalidDataConversionException.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception;
+
+/**
+ * InvalidDataConversionException class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class InvalidDataConversionException extends RuntimeException {
+ private static final long serialVersionUID = -1;
+
+ /**
+ * Constructor for InvalidDataConversionException.
+ *
+ * @param message a {@link String} object.
+ */
+ public InvalidDataConversionException(String message) {
+ super(message);
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/exception/ModbusIdException.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/exception/ModbusIdException.java
new file mode 100644
index 0000000..abea265
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/exception/ModbusIdException.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception;
+
+/**
+ * ModbusIdException class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class ModbusIdException extends RuntimeException {
+ private static final long serialVersionUID = -1;
+
+ /**
+ * Constructor for ModbusIdException.
+ *
+ * @param message a {@link String} object.
+ */
+ public ModbusIdException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructor for ModbusIdException.
+ *
+ * @param cause a {@link Throwable} object.
+ */
+ public ModbusIdException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/exception/ModbusInitException.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/exception/ModbusInitException.java
new file mode 100644
index 0000000..f1d371e
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/exception/ModbusInitException.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception;
+
+/**
+ * ModbusInitException class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class ModbusInitException extends Exception {
+ private static final long serialVersionUID = -1;
+
+ /**
+ * Constructor for ModbusInitException.
+ */
+ public ModbusInitException() {
+ super();
+ }
+
+ /**
+ * Constructor for ModbusInitException.
+ *
+ * @param message a {@link String} object.
+ * @param cause a {@link Throwable} object.
+ */
+ public ModbusInitException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * Constructor for ModbusInitException.
+ *
+ * @param message a {@link String} object.
+ */
+ public ModbusInitException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructor for ModbusInitException.
+ *
+ * @param cause a {@link Throwable} object.
+ */
+ public ModbusInitException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/exception/ModbusTransportException.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/exception/ModbusTransportException.java
new file mode 100644
index 0000000..c13e4e6
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/exception/ModbusTransportException.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception;
+
+/**
+ * ModbusTransportException class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class ModbusTransportException extends Exception {
+ private static final long serialVersionUID = -1;
+
+ private final int slaveId;
+
+ /**
+ * Constructor for ModbusTransportException.
+ */
+ public ModbusTransportException() {
+ this.slaveId = -1;
+ }
+
+ /**
+ * Constructor for ModbusTransportException.
+ *
+ * @param slaveId a int.
+ */
+ public ModbusTransportException(int slaveId) {
+ this.slaveId = slaveId;
+ }
+
+ /**
+ * Constructor for ModbusTransportException.
+ *
+ * @param message a {@link String} object.
+ * @param cause a {@link Throwable} object.
+ * @param slaveId a int.
+ */
+ public ModbusTransportException(String message, Throwable cause, int slaveId) {
+ super(message, cause);
+ this.slaveId = slaveId;
+ }
+
+ /**
+ * Constructor for ModbusTransportException.
+ *
+ * @param message a {@link String} object.
+ * @param slaveId a int.
+ */
+ public ModbusTransportException(String message, int slaveId) {
+ super(message);
+ this.slaveId = slaveId;
+ }
+
+ /**
+ * Constructor for ModbusTransportException.
+ *
+ * @param message a {@link String} object.
+ */
+ public ModbusTransportException(String message) {
+ super(message);
+ this.slaveId = -1;
+ }
+
+ /**
+ * Constructor for ModbusTransportException.
+ *
+ * @param cause a {@link Throwable} object.
+ */
+ public ModbusTransportException(Throwable cause) {
+ super(cause);
+ this.slaveId = -1;
+ }
+
+ /**
+ * Constructor for ModbusTransportException.
+ *
+ * @param cause a {@link Throwable} object.
+ * @param slaveId a int.
+ */
+ public ModbusTransportException(Throwable cause, int slaveId) {
+ super(cause);
+ this.slaveId = slaveId;
+ }
+
+ /**
+ * Getter for the field slaveId.
+ *
+ * @return a int.
+ */
+ public int getSlaveId() {
+ return slaveId;
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/exception/SlaveIdNotEqual.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/exception/SlaveIdNotEqual.java
new file mode 100644
index 0000000..a4053b6
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/exception/SlaveIdNotEqual.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception;
+
+public class SlaveIdNotEqual extends ModbusTransportException {
+ private static final long serialVersionUID = -1;
+
+ /**
+ * Exception to show that the requested slave id is not what was received
+ *
+ * @param requestSlaveId - slave id requested
+ * @param responseSlaveId - slave id of response
+ */
+ public SlaveIdNotEqual(int requestSlaveId, int responseSlaveId) {
+ super("Response slave id different from requested id", requestSlaveId);
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/IpMessage.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/IpMessage.java
new file mode 100644
index 0000000..650ab33
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/IpMessage.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusMessage;
+
+/**
+ * Abstract IpMessage class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+abstract public class IpMessage {
+ protected final ModbusMessage modbusMessage;
+
+ /**
+ * Constructor for IpMessage.
+ *
+ * @param modbusMessage a {@link ModbusMessage} object.
+ */
+ public IpMessage(ModbusMessage modbusMessage) {
+ this.modbusMessage = modbusMessage;
+ }
+
+ /**
+ * Getter for the field modbusMessage.
+ *
+ * @return a {@link ModbusMessage} object.
+ */
+ public ModbusMessage getModbusMessage() {
+ return modbusMessage;
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/IpMessageResponse.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/IpMessageResponse.java
new file mode 100644
index 0000000..2f995ea
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/IpMessageResponse.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusResponse;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.IncomingResponseMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.OutgoingResponseMessage;
+
+/**
+ * IpMessageResponse interface.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public interface IpMessageResponse extends OutgoingResponseMessage, IncomingResponseMessage {
+ /**
+ * getModbusResponse.
+ *
+ * @return a {@link ModbusResponse} object.
+ */
+ ModbusResponse getModbusResponse();
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/IpParameters.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/IpParameters.java
new file mode 100644
index 0000000..52a74ca
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/IpParameters.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.ModbusUtils;
+
+/**
+ * IpParameters class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class IpParameters {
+ private String host;
+ private int port = ModbusUtils.TCP_PORT;
+ private boolean encapsulated;
+ private Integer lingerTime = -1;
+
+
+ /**
+ * Getter for the field host.
+ *
+ * @return a {@link String} object.
+ */
+ public String getHost() {
+ return host;
+ }
+
+ /**
+ * Setter for the field host.
+ *
+ * @param host a {@link String} object.
+ */
+ public void setHost(String host) {
+ this.host = host;
+ }
+
+ /**
+ * Getter for the field port.
+ *
+ * @return a int.
+ */
+ public int getPort() {
+ return port;
+ }
+
+ /**
+ * Setter for the field port.
+ *
+ * @param port a int.
+ */
+ public void setPort(int port) {
+ this.port = port;
+ }
+
+ /**
+ * isEncapsulated.
+ *
+ * @return a boolean.
+ */
+ public boolean isEncapsulated() {
+ return encapsulated;
+ }
+
+ /**
+ * Setter for the field encapsulated.
+ *
+ * @param encapsulated a boolean.
+ */
+ public void setEncapsulated(boolean encapsulated) {
+ this.encapsulated = encapsulated;
+ }
+
+ /**
+ * Getter for the field linger.
+ *
+ * @return a int.
+ */
+ public Integer getLingerTime() {
+ return lingerTime;
+ }
+
+ /**
+ * Setter for the field linger.
+ *
+ * @param lingerTime a int.
+ */
+ public void setLingerTime(Integer lingerTime) {
+ this.lingerTime = lingerTime;
+ }
+
+
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/encap/EncapMessage.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/encap/EncapMessage.java
new file mode 100644
index 0000000..a227cd5
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/encap/EncapMessage.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.encap;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.ModbusUtils;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.IpMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * EncapMessage class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class EncapMessage extends IpMessage {
+ /**
+ * Constructor for EncapMessage.
+ *
+ * @param modbusMessage a {@link ModbusMessage} object.
+ */
+ public EncapMessage(ModbusMessage modbusMessage) {
+ super(modbusMessage);
+ }
+
+ /**
+ * getMessageData.
+ *
+ * @return an array of {@link byte} objects.
+ */
+ public byte[] getMessageData() {
+ ByteQueue msgQueue = new ByteQueue();
+
+ // Write the particular message.
+ modbusMessage.write(msgQueue);
+
+ // Write the CRC
+ ModbusUtils.pushShort(msgQueue, ModbusUtils.calculateCRC(modbusMessage));
+
+ // Return the data.
+ return msgQueue.popAll();
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/encap/EncapMessageParser.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/encap/EncapMessageParser.java
new file mode 100644
index 0000000..26b06e7
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/encap/EncapMessageParser.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.encap;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.BaseMessageParser;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.IncomingMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * EncapMessageParser class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class EncapMessageParser extends BaseMessageParser {
+ /**
+ * Constructor for EncapMessageParser.
+ *
+ * @param master a boolean.
+ */
+ public EncapMessageParser(boolean master) {
+ super(master);
+ }
+
+ @Override
+ protected IncomingMessage parseMessageImpl(ByteQueue queue) throws Exception {
+ if (master)
+ return EncapMessageResponse.createEncapMessageResponse(queue);
+ return EncapMessageRequest.createEncapMessageRequest(queue);
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/encap/EncapMessageRequest.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/encap/EncapMessageRequest.java
new file mode 100644
index 0000000..4364d22
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/encap/EncapMessageRequest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.encap;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.ModbusUtils;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusRequest;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.IncomingRequestMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.OutgoingRequestMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * EncapMessageRequest class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class EncapMessageRequest extends EncapMessage implements OutgoingRequestMessage, IncomingRequestMessage {
+ /**
+ * Constructor for EncapMessageRequest.
+ *
+ * @param modbusRequest a {@link ModbusRequest} object.
+ */
+ public EncapMessageRequest(ModbusRequest modbusRequest) {
+ super(modbusRequest);
+ }
+
+ static EncapMessageRequest createEncapMessageRequest(ByteQueue queue) throws ModbusTransportException {
+ // Create the modbus response.
+ ModbusRequest request = ModbusRequest.createModbusRequest(queue);
+ EncapMessageRequest encapRequest = new EncapMessageRequest(request);
+
+ // Check the CRC
+ ModbusUtils.checkCRC(encapRequest.modbusMessage, queue);
+
+ return encapRequest;
+ }
+
+ @Override
+ public boolean expectsResponse() {
+ return modbusMessage.getSlaveId() != 0;
+ }
+
+ /**
+ * getModbusRequest.
+ *
+ * @return a {@link ModbusRequest} object.
+ */
+ public ModbusRequest getModbusRequest() {
+ return (ModbusRequest) modbusMessage;
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/encap/EncapMessageResponse.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/encap/EncapMessageResponse.java
new file mode 100644
index 0000000..3bbc4c5
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/encap/EncapMessageResponse.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.encap;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.ModbusUtils;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.IpMessageResponse;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusResponse;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * EncapMessageResponse class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class EncapMessageResponse extends EncapMessage implements IpMessageResponse {
+ /**
+ * Constructor for EncapMessageResponse.
+ *
+ * @param modbusResponse a {@link ModbusResponse} object.
+ */
+ public EncapMessageResponse(ModbusResponse modbusResponse) {
+ super(modbusResponse);
+ }
+
+ static EncapMessageResponse createEncapMessageResponse(ByteQueue queue) throws ModbusTransportException {
+ // Create the modbus response.
+ ModbusResponse response = ModbusResponse.createModbusResponse(queue);
+ EncapMessageResponse encapResponse = new EncapMessageResponse(response);
+
+ // Check the CRC
+ ModbusUtils.checkCRC(encapResponse.modbusMessage, queue);
+
+ return encapResponse;
+ }
+
+ /**
+ * getModbusResponse.
+ *
+ * @return a {@link ModbusResponse} object.
+ */
+ public ModbusResponse getModbusResponse() {
+ return (ModbusResponse) modbusMessage;
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/encap/EncapRequestHandler.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/encap/EncapRequestHandler.java
new file mode 100644
index 0000000..540795f
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/encap/EncapRequestHandler.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.encap;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ModbusSlaveSet;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.BaseRequestHandler;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusRequest;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusResponse;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.IncomingRequestMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.OutgoingResponseMessage;
+
+/**
+ * EncapRequestHandler class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class EncapRequestHandler extends BaseRequestHandler {
+ /**
+ * Constructor for EncapRequestHandler.
+ *
+ * @param slave a {@link ModbusSlaveSet} object.
+ */
+ public EncapRequestHandler(ModbusSlaveSet slave) {
+ super(slave);
+ }
+
+
+ public OutgoingResponseMessage handleRequest(IncomingRequestMessage req) throws Exception {
+ EncapMessageRequest tcpRequest = (EncapMessageRequest) req;
+ ModbusRequest request = tcpRequest.getModbusRequest();
+ ModbusResponse response = handleRequestImpl(request);
+ if (response == null)
+ return null;
+ return new EncapMessageResponse(response);
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/encap/EncapWaitingRoomKeyFactory.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/encap/EncapWaitingRoomKeyFactory.java
new file mode 100644
index 0000000..dc76e5c
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/encap/EncapWaitingRoomKeyFactory.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.encap;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.IpMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.IncomingResponseMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.OutgoingRequestMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.WaitingRoomKey;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.WaitingRoomKeyFactory;
+
+/**
+ * EncapWaitingRoomKeyFactory class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class EncapWaitingRoomKeyFactory implements WaitingRoomKeyFactory {
+ @Override
+ public WaitingRoomKey createWaitingRoomKey(OutgoingRequestMessage request) {
+ return createWaitingRoomKey(((IpMessage) request).getModbusMessage());
+ }
+
+ @Override
+ public WaitingRoomKey createWaitingRoomKey(IncomingResponseMessage response) {
+ return createWaitingRoomKey(((IpMessage) response).getModbusMessage());
+ }
+
+ /**
+ * createWaitingRoomKey.
+ *
+ * @param msg a {@link ModbusMessage} object.
+ * @return a {@link WaitingRoomKey} object.
+ */
+ public WaitingRoomKey createWaitingRoomKey(ModbusMessage msg) {
+ return new EncapWaitingRoomKey(msg.getSlaveId(), msg.getFunctionCode());
+ }
+
+ class EncapWaitingRoomKey implements WaitingRoomKey {
+ private final int slaveId;
+ private final byte functionCode;
+
+ public EncapWaitingRoomKey(int slaveId, byte functionCode) {
+ this.slaveId = slaveId;
+ this.functionCode = functionCode;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + functionCode;
+ result = prime * result + slaveId;
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ EncapWaitingRoomKey other = (EncapWaitingRoomKey) obj;
+ if (functionCode != other.functionCode)
+ return false;
+ if (slaveId != other.slaveId)
+ return false;
+ return true;
+ }
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/listener/TcpListener.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/listener/TcpListener.java
new file mode 100644
index 0000000..946863e
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/listener/TcpListener.java
@@ -0,0 +1,357 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.listener;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ModbusMaster;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.BaseMessageParser;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusInitException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.IpMessageResponse;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.IpParameters;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.encap.EncapMessageParser;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.encap.EncapMessageRequest;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.encap.EncapWaitingRoomKeyFactory;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.xa.XaMessageParser;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.xa.XaMessageRequest;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.xa.XaWaitingRoomKeyFactory;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusRequest;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusResponse;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.*;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import java.io.IOException;
+import java.net.BindException;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+import java.util.Arrays;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * TcpListener class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class TcpListener extends ModbusMaster {
+ // Configuration fields.
+ private final Log LOG = LogFactory.getLog(TcpListener.class);
+ private final IpParameters ipParameters;
+ private short nextTransactionId = 0;
+ private short retries = 0;
+ // Runtime fields.
+ private ServerSocket serverSocket;
+ private Socket socket;
+ private ExecutorService executorService;
+ private ListenerConnectionHandler handler;
+
+ /**
+ * Constructor for TcpListener.
+ *
+ * Will validate response to ensure that slaveId == response slaveId if encapsulated is true
+ *
+ * @param params a {@link IpParameters} object.
+ */
+ public TcpListener(IpParameters params) {
+ LOG.debug("Creating TcpListener in port " + params.getPort());
+ ipParameters = params;
+ connected = false;
+ validateResponse = ipParameters.isEncapsulated();
+ if (LOG.isDebugEnabled())
+ LOG.debug("TcpListener created! Port: " + ipParameters.getPort());
+ }
+
+ /**
+ * Control to validate response to ensure that slaveId == response slaveId
+ *
+ * @param params a {@link IpParameters} object.
+ * @param validateResponse a boolean.
+ */
+ public TcpListener(IpParameters params, boolean validateResponse) {
+ LOG.debug("Creating TcpListener in port " + params.getPort());
+ ipParameters = params;
+ connected = false;
+ this.validateResponse = validateResponse;
+ if (LOG.isDebugEnabled())
+ LOG.debug("TcpListener created! Port: " + ipParameters.getPort());
+ }
+
+ /**
+ *
Getter for the field nextTransactionId.
+ *
+ * @return a short.
+ */
+ protected short getNextTransactionId() {
+ return nextTransactionId++;
+ }
+
+
+ @Override
+ synchronized public void init() throws ModbusInitException {
+ LOG.debug("Init TcpListener Port: " + ipParameters.getPort());
+ executorService = Executors.newCachedThreadPool();
+ startListener();
+ initialized = true;
+ LOG.warn("Initialized Port: " + ipParameters.getPort());
+ }
+
+ private void startListener() throws ModbusInitException {
+ try {
+ if (handler != null) {
+ LOG.debug("handler not null!!!");
+ }
+ handler = new ListenerConnectionHandler(socket);
+ LOG.debug("Init handler thread");
+ executorService.execute(handler);
+ } catch (Exception e) {
+ LOG.warn("Error initializing TcpListener ", e);
+ throw new ModbusInitException(e);
+ }
+ }
+
+
+ @Override
+ synchronized public void destroy() {
+ LOG.debug("Destroy TCPListener Port: " + ipParameters.getPort());
+ // Close the serverSocket first to prevent new messages.
+ try {
+ if (serverSocket != null)
+ serverSocket.close();
+ } catch (IOException e) {
+ LOG.warn("Error closing socket" + e.getLocalizedMessage());
+ getExceptionHandler().receivedException(e);
+ }
+
+ // Close all open connections.
+ if (handler != null) {
+ handler.closeConnection();
+ }
+
+ // Terminate Listener
+ terminateListener();
+ initialized = false;
+ LOG.debug("TCPListener destroyed, Port: " + ipParameters.getPort());
+ }
+
+ private void terminateListener() {
+ executorService.shutdown();
+ try {
+ executorService.awaitTermination(300, TimeUnit.MILLISECONDS);
+ LOG.debug("Handler Thread terminated, Port: " + ipParameters.getPort());
+ } catch (InterruptedException e) {
+ LOG.debug("Error terminating executorService - " + e.getLocalizedMessage());
+ getExceptionHandler().receivedException(e);
+ }
+ handler = null;
+ }
+
+
+ @Override
+ synchronized public ModbusResponse sendImpl(ModbusRequest request) throws ModbusTransportException {
+
+ if (!connected) {
+ LOG.debug("No connection in Port: " + ipParameters.getPort());
+ throw new ModbusTransportException(new Exception("TCP Listener has no active connection!"),
+ request.getSlaveId());
+ }
+
+ if (!initialized) {
+ LOG.debug("Listener already terminated " + ipParameters.getPort());
+ return null;
+ }
+
+ // Wrap the modbus request in a ip request.
+ OutgoingRequestMessage ipRequest;
+ if (ipParameters.isEncapsulated()) {
+ ipRequest = new EncapMessageRequest(request);
+ StringBuilder sb = new StringBuilder();
+ for (byte b : Arrays.copyOfRange(ipRequest.getMessageData(), 0, ipRequest.getMessageData().length)) {
+ sb.append(String.format("%02X ", b));
+ }
+ LOG.debug("Encap Request: " + sb.toString());
+ } else {
+ ipRequest = new XaMessageRequest(request, getNextTransactionId());
+ StringBuilder sb = new StringBuilder();
+ for (byte b : Arrays.copyOfRange(ipRequest.getMessageData(), 0, ipRequest.getMessageData().length)) {
+ sb.append(String.format("%02X ", b));
+ }
+ LOG.debug("Xa Request: " + sb.toString());
+ }
+
+ // Send the request to get the response.
+ IpMessageResponse ipResponse;
+ try {
+ // Send data via handler!
+ handler.conn.DEBUG = true;
+ ipResponse = (IpMessageResponse) handler.conn.send(ipRequest);
+ if (ipResponse == null) {
+ throw new ModbusTransportException(new Exception("No valid response from slave!"), request.getSlaveId());
+ }
+ StringBuilder sb = new StringBuilder();
+ for (byte b : Arrays.copyOfRange(ipResponse.getMessageData(), 0, ipResponse.getMessageData().length)) {
+ sb.append(String.format("%02X ", b));
+ }
+ LOG.debug("Response: " + sb.toString());
+ return ipResponse.getModbusResponse();
+ } catch (Exception e) {
+ LOG.debug(e.getLocalizedMessage() + ", Port: " + ipParameters.getPort() + ", retries: " + retries);
+ if (retries < 10 && !e.getLocalizedMessage().contains("Broken")) {
+ retries++;
+ } else {
+ /*
+ * To recover from a Broken Pipe, the only way is to restart serverSocket
+ */
+ LOG.debug("Restarting Socket, Port: " + ipParameters.getPort() + ", retries: " + retries);
+
+ // Close the serverSocket first to prevent new messages.
+ try {
+ if (serverSocket != null)
+ serverSocket.close();
+ } catch (IOException e2) {
+ LOG.debug("Error closing socket" + e2.getLocalizedMessage(), e);
+ getExceptionHandler().receivedException(e2);
+ }
+
+ // Close all open connections.
+ if (handler != null) {
+ handler.closeConnection();
+ terminateListener();
+ }
+
+ if (!initialized) {
+ LOG.debug("Listener already terminated " + ipParameters.getPort());
+ return null;
+ }
+
+ executorService = Executors.newCachedThreadPool();
+ try {
+ startListener();
+ } catch (Exception e2) {
+ LOG.warn("Error trying to restart socket" + e2.getLocalizedMessage(), e);
+ throw new ModbusTransportException(e2, request.getSlaveId());
+ }
+ retries = 0;
+ }
+ LOG.warn("Error sending request, Port: " + ipParameters.getPort() + ", msg: " + e.getMessage());
+ // Simple send error!
+ throw new ModbusTransportException(e, request.getSlaveId());
+ }
+ }
+
+ class ListenerConnectionHandler implements Runnable {
+ private Socket socket;
+ private Transport transport;
+ private MessageControl conn;
+ private BaseMessageParser ipMessageParser;
+ private WaitingRoomKeyFactory waitingRoomKeyFactory;
+
+ public ListenerConnectionHandler(Socket socket) {
+ this.socket = socket;
+ }
+
+ @Override
+ public void run() {
+ LOG.debug(" ListenerConnectionHandler::run() ");
+
+ if (ipParameters.isEncapsulated()) {
+ ipMessageParser = new EncapMessageParser(true);
+ waitingRoomKeyFactory = new EncapWaitingRoomKeyFactory();
+ } else {
+ ipMessageParser = new XaMessageParser(true);
+ waitingRoomKeyFactory = new XaWaitingRoomKeyFactory();
+ }
+
+ try {
+ acceptConnection();
+ } catch (IOException e) {
+ LOG.debug("Error in TCP Listener! - " + e.getLocalizedMessage(), e);
+ conn.close();
+ closeConnection();
+ getExceptionHandler().receivedException(new ModbusInitException(e));
+ }
+ }
+
+ private void acceptConnection() throws IOException, BindException {
+ while (true) {
+ try {
+ Thread.sleep(500);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ if (!connected) {
+ try {
+ serverSocket = new ServerSocket(ipParameters.getPort());
+ LOG.debug("Start Accept on port: " + ipParameters.getPort());
+ socket = serverSocket.accept();
+ LOG.info("Connected: " + socket.getInetAddress() + ":" + ipParameters.getPort());
+
+ if (getePoll() != null)
+ transport = new EpollStreamTransport(socket.getInputStream(), socket.getOutputStream(),
+ getePoll());
+ else
+ transport = new StreamTransport(socket.getInputStream(), socket.getOutputStream());
+ break;
+ } catch (Exception e) {
+ LOG.warn(
+ "Open connection failed on port " + ipParameters.getPort() + ", caused by "
+ + e.getLocalizedMessage(), e);
+ if (e instanceof SocketTimeoutException) {
+ continue;
+ } else if (e.getLocalizedMessage().contains("closed")) {
+ return;
+ } else if (e instanceof BindException) {
+ closeConnection();
+ throw (BindException) e;
+ }
+ }
+ }
+ }
+
+ conn = getMessageControl();
+ conn.setExceptionHandler(getExceptionHandler());
+ conn.DEBUG = true;
+ conn.start(transport, ipMessageParser, null, waitingRoomKeyFactory);
+ if (getePoll() == null)
+ ((StreamTransport) transport).start("Modbus4J TcpMaster");
+ connected = true;
+ }
+
+ void closeConnection() {
+ if (conn != null) {
+ LOG.debug("Closing Message Control on port: " + ipParameters.getPort());
+ closeMessageControl(conn);
+ }
+
+ try {
+ if (socket != null) {
+ socket.close();
+ }
+ } catch (IOException e) {
+ LOG.debug("Error closing socket on port " + ipParameters.getPort() + ". " + e.getLocalizedMessage());
+ getExceptionHandler().receivedException(new ModbusInitException(e));
+ }
+ connected = false;
+ conn = null;
+ socket = null;
+ }
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/tcp/TcpMaster.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/tcp/TcpMaster.java
new file mode 100644
index 0000000..d2d561f
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/tcp/TcpMaster.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.tcp;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ModbusMaster;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.BaseMessageParser;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusInitException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.IpMessageResponse;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.IpParameters;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.encap.EncapMessageParser;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.encap.EncapMessageRequest;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.encap.EncapWaitingRoomKeyFactory;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.xa.XaMessageParser;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.xa.XaMessageRequest;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.xa.XaWaitingRoomKeyFactory;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusRequest;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusResponse;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.*;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.util.Arrays;
+
+/**
+ * TcpMaster class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class TcpMaster extends ModbusMaster {
+
+ // Configuration fields.
+ private final Log LOG = LogFactory.getLog(TcpMaster.class);
+ private final IpParameters ipParameters;
+ private final boolean keepAlive;
+ private final boolean autoIncrementTransactionId;
+ private final Integer lingerTime;
+ private short nextTransactionId = 0;
+ // Runtime fields.
+ private Socket socket;
+ private Transport transport;
+ private MessageControl conn;
+
+
+ /**
+ * Constructor for TcpMaster.
+ *
+ * @param params a {@link IpParameters} object.
+ * @param keepAlive a boolean.
+ * @param autoIncrementTransactionId a boolean.
+ * @param validateResponse - confirm that requested slave id is the same in the response
+ * @param lingerTime The setting only affects socket close.
+ */
+ public TcpMaster(IpParameters params, boolean keepAlive, boolean autoIncrementTransactionId, boolean validateResponse, Integer lingerTime) {
+ this.ipParameters = params;
+ this.keepAlive = keepAlive;
+ this.autoIncrementTransactionId = autoIncrementTransactionId;
+ this.lingerTime = lingerTime;
+ }
+
+ /**
+ * Constructor for TcpMaster.
+ *
+ * Default to lingerTime disabled
+ *
+ * @param params a {@link IpParameters} object.
+ * @param keepAlive a boolean.
+ * @param autoIncrementTransactionId a boolean.
+ * @param validateResponse - confirm that requested slave id is the same in the response
+ */
+ public TcpMaster(IpParameters params, boolean keepAlive, boolean autoIncrementTransactionId, boolean validateResponse) {
+ this(params, keepAlive, autoIncrementTransactionId, validateResponse, -1);
+ //this.ipParameters = params;
+ //this.keepAlive = keepAlive;
+ //this.autoIncrementTransactionId = autoIncrementTransactionId;
+ }
+
+ /**
+ *
Constructor for TcpMaster.
+ * Default to not validating the slave id in responses
+ * Default to lingerTime disabled
+ *
+ * @param params a {@link IpParameters} object.
+ * @param keepAlive a boolean.
+ * @param autoIncrementTransactionId a boolean.
+ */
+ public TcpMaster(IpParameters params, boolean keepAlive, boolean autoIncrementTransactionId) {
+ this(params, keepAlive, autoIncrementTransactionId, false, -1);
+ }
+
+ /**
+ * Constructor for TcpMaster.
+ *
+ * Default to auto increment transaction id
+ * Default to not validating the slave id in responses
+ * Default to lingerTime disabled
+ *
+ * @param params a {@link IpParameters} object.
+ * @param keepAlive a boolean.
+ * @param lingerTime an Integer. The setting only affects socket close.
+ */
+ public TcpMaster(IpParameters params, boolean keepAlive, Integer lingerTime) {
+ this(params, keepAlive, true, false, lingerTime);
+ }
+
+ /**
+ *
Constructor for TcpMaster.
+ *
+ * Default to auto increment transaction id
+ * Default to not validating the slave id in responses
+ * Default to lingerTime disabled
+ *
+ * @param params a {@link IpParameters} object.
+ * @param keepAlive a boolean.
+ */
+ public TcpMaster(IpParameters params, boolean keepAlive) {
+ this(params, keepAlive, true, false, -1);
+ }
+
+ /**
+ *
Getter for the field nextTransactionId.
+ *
+ * @return a short.
+ */
+ protected short getNextTransactionId() {
+ return nextTransactionId;
+ }
+
+ /**
+ * Setter for the field nextTransactionId.
+ *
+ * @param id a short.
+ */
+ public void setNextTransactionId(short id) {
+ this.nextTransactionId = id;
+ }
+
+ @Override
+ synchronized public void init() throws ModbusInitException {
+ try {
+ if (keepAlive)
+ openConnection();
+ } catch (Exception e) {
+ throw new ModbusInitException(e);
+ }
+ initialized = true;
+ }
+
+ @Override
+ synchronized public void destroy() {
+ closeConnection();
+ initialized = false;
+ }
+
+ @Override
+ synchronized public ModbusResponse sendImpl(ModbusRequest request) throws ModbusTransportException {
+ try {
+ // Check if we need to open the connection.
+ if (!keepAlive)
+ openConnection();
+
+ if (conn == null) {
+ LOG.debug("Connection null: " + ipParameters.getPort());
+ }
+
+ } catch (Exception e) {
+ closeConnection();
+ throw new ModbusTransportException(e, request.getSlaveId());
+ }
+
+ // Wrap the modbus request in a ip request.
+ OutgoingRequestMessage ipRequest;
+ if (ipParameters.isEncapsulated())
+ ipRequest = new EncapMessageRequest(request);
+ else {
+ if (autoIncrementTransactionId)
+ this.nextTransactionId++;
+ ipRequest = new XaMessageRequest(request, getNextTransactionId());
+ }
+
+ if (LOG.isDebugEnabled()) {
+ StringBuilder sb = new StringBuilder();
+ for (byte b : Arrays.copyOfRange(ipRequest.getMessageData(), 0, ipRequest.getMessageData().length)) {
+ sb.append(String.format("%02X ", b));
+ }
+ LOG.debug("Encap Request: " + sb.toString());
+ }
+
+ // Send the request to get the response.
+ IpMessageResponse ipResponse;
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Sending on port: " + ipParameters.getPort());
+ }
+ try {
+ if (conn == null) {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Connection null: " + ipParameters.getPort());
+ }
+ ipResponse = (IpMessageResponse) conn.send(ipRequest);
+ if (ipResponse == null)
+ return null;
+
+ if (LOG.isDebugEnabled()) {
+ StringBuilder sb = new StringBuilder();
+ for (byte b : Arrays.copyOfRange(ipResponse.getMessageData(), 0, ipResponse.getMessageData().length)) {
+ sb.append(String.format("%02X ", b));
+ }
+ LOG.debug("Response: " + sb.toString());
+ }
+ return ipResponse.getModbusResponse();
+ } catch (Exception e) {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Exception sending message", e);
+ if (keepAlive) {
+ if (LOG.isDebugEnabled())
+ LOG.debug("KeepAlive - reconnect!");
+ // The connection may have been reset, so try to reopen it and attempt the message again.
+ try {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Modbus4J: Keep-alive connection may have been reset. Attempting to re-open.");
+ openConnection();
+ ipResponse = (IpMessageResponse) conn.send(ipRequest);
+ if (ipResponse == null)
+ return null;
+ if (LOG.isDebugEnabled()) {
+ StringBuilder sb = new StringBuilder();
+ for (byte b : Arrays.copyOfRange(ipResponse.getMessageData(), 0, ipResponse.getMessageData().length)) {
+ sb.append(String.format("%02X ", b));
+ }
+ LOG.debug("Response: " + sb.toString());
+ }
+ return ipResponse.getModbusResponse();
+ } catch (Exception e2) {
+ closeConnection();
+ if (LOG.isDebugEnabled())
+ LOG.debug("Exception re-sending message", e);
+ throw new ModbusTransportException(e2, request.getSlaveId());
+ }
+ }
+
+ throw new ModbusTransportException(e, request.getSlaveId());
+ } finally {
+ // Check if we should close the connection.
+ if (!keepAlive)
+ closeConnection();
+ }
+ }
+
+ //
+ //
+ // Private methods
+ //
+ private void openConnection() throws IOException {
+ // Make sure any existing connection is closed.
+ closeConnection();
+
+ Integer soLinger = getLingerTime();
+
+ socket = new Socket();
+ socket.setSoTimeout(getTimeout());
+ if (soLinger == null || soLinger < 0)//any null or negative will disable SO_Linger
+ socket.setSoLinger(false, 0);
+ else
+ socket.setSoLinger(true, soLinger);
+ socket.connect(new InetSocketAddress(ipParameters.getHost(), ipParameters.getPort()), getTimeout());
+ if (getePoll() != null)
+ transport = new EpollStreamTransport(socket.getInputStream(), socket.getOutputStream(), getePoll());
+ else
+ transport = new StreamTransport(socket.getInputStream(), socket.getOutputStream());
+
+ BaseMessageParser ipMessageParser;
+ WaitingRoomKeyFactory waitingRoomKeyFactory;
+ if (ipParameters.isEncapsulated()) {
+ ipMessageParser = new EncapMessageParser(true);
+ waitingRoomKeyFactory = new EncapWaitingRoomKeyFactory();
+ } else {
+ ipMessageParser = new XaMessageParser(true);
+ waitingRoomKeyFactory = new XaWaitingRoomKeyFactory();
+ }
+
+ conn = getMessageControl();
+ conn.start(transport, ipMessageParser, null, waitingRoomKeyFactory);
+ if (getePoll() == null)
+ ((StreamTransport) transport).start("Modbus4J TcpMaster");
+ }
+
+ private void closeConnection() {
+ closeMessageControl(conn);
+ try {
+ if (socket != null)
+ socket.close();
+ } catch (IOException e) {
+ getExceptionHandler().receivedException(e);
+ }
+
+ conn = null;
+ socket = null;
+ }
+
+ /**
+ * Getter for the field lingerTime.
+ *
+ * @return an Integer.
+ */
+ public Integer getLingerTime() {
+ return lingerTime;
+ }
+
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/tcp/TcpSlave.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/tcp/TcpSlave.java
new file mode 100644
index 0000000..365e4e8
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/tcp/TcpSlave.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.tcp;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ModbusSlaveSet;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.BaseMessageParser;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.BaseRequestHandler;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.ModbusUtils;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusInitException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.encap.EncapMessageParser;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.encap.EncapRequestHandler;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.xa.XaMessageParser;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.xa.XaRequestHandler;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.MessageControl;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.TestableTransport;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * TcpSlave class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class TcpSlave extends ModbusSlaveSet {
+ final boolean encapsulated;
+ final ExecutorService executorService;
+ final List listConnections = new ArrayList<>();
+ // Configuration fields
+ private final int port;
+ // Runtime fields.
+ private ServerSocket serverSocket;
+
+ /**
+ * Constructor for TcpSlave.
+ *
+ * @param encapsulated a boolean.
+ */
+ public TcpSlave(boolean encapsulated) {
+ this(ModbusUtils.TCP_PORT, encapsulated);
+ }
+
+ /**
+ * Constructor for TcpSlave.
+ *
+ * @param port a int.
+ * @param encapsulated a boolean.
+ */
+ public TcpSlave(int port, boolean encapsulated) {
+ this.port = port;
+ this.encapsulated = encapsulated;
+ executorService = Executors.newCachedThreadPool();
+ }
+
+ @Override
+ public void start() throws ModbusInitException {
+ try {
+ serverSocket = new ServerSocket(port);
+
+ Socket socket;
+ while (true) {
+ socket = serverSocket.accept();
+ TcpConnectionHandler handler = new TcpConnectionHandler(socket);
+ executorService.execute(handler);
+ synchronized (listConnections) {
+ listConnections.add(handler);
+ }
+ }
+ } catch (IOException e) {
+ throw new ModbusInitException(e);
+ }
+ }
+
+ @Override
+ public void stop() {
+ // Close the socket first to prevent new messages.
+ try {
+ serverSocket.close();
+ } catch (IOException e) {
+ getExceptionHandler().receivedException(e);
+ }
+
+ // Close all open connections.
+ synchronized (listConnections) {
+ for (TcpConnectionHandler tch : listConnections)
+ tch.kill();
+ listConnections.clear();
+ }
+
+ // Now close the executor service.
+ executorService.shutdown();
+ try {
+ executorService.awaitTermination(3, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ getExceptionHandler().receivedException(e);
+ }
+ }
+
+ class TcpConnectionHandler implements Runnable {
+ private final Socket socket;
+ private TestableTransport transport;
+ private MessageControl conn;
+
+ TcpConnectionHandler(Socket socket) throws ModbusInitException {
+ this.socket = socket;
+ try {
+ transport = new TestableTransport(socket.getInputStream(), socket.getOutputStream());
+ } catch (IOException e) {
+ throw new ModbusInitException(e);
+ }
+ }
+
+ @Override
+ public void run() {
+ BaseMessageParser messageParser;
+ BaseRequestHandler requestHandler;
+
+ if (encapsulated) {
+ messageParser = new EncapMessageParser(false);
+ requestHandler = new EncapRequestHandler(TcpSlave.this);
+ } else {
+ messageParser = new XaMessageParser(false);
+ requestHandler = new XaRequestHandler(TcpSlave.this);
+ }
+
+ conn = new MessageControl();
+ conn.setExceptionHandler(getExceptionHandler());
+
+ try {
+ conn.start(transport, messageParser, requestHandler, null);
+ executorService.execute(transport);
+ } catch (IOException e) {
+ getExceptionHandler().receivedException(new ModbusInitException(e));
+ }
+
+ // Monitor the socket to detect when it gets closed.
+ while (true) {
+ try {
+ transport.testInputStream();
+ } catch (IOException e) {
+ break;
+ }
+
+ try {
+ Thread.sleep(500);
+ } catch (InterruptedException e) {
+ // no op
+ }
+ }
+
+ conn.close();
+ kill();
+ synchronized (listConnections) {
+ listConnections.remove(this);
+ }
+ }
+
+ void kill() {
+ try {
+ socket.close();
+ } catch (IOException e) {
+ getExceptionHandler().receivedException(new ModbusInitException(e));
+ }
+ }
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/udp/UdpMaster.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/udp/UdpMaster.java
new file mode 100644
index 0000000..3b0a3ec
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/udp/UdpMaster.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.udp;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ModbusMaster;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.BaseMessageParser;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusInitException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.IpMessageResponse;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.IpParameters;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.encap.EncapMessageParser;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.encap.EncapMessageRequest;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.xa.XaMessageParser;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.xa.XaMessageRequest;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusRequest;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusResponse;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.OutgoingRequestMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+import java.io.IOException;
+import java.net.*;
+
+/**
+ * UdpMaster class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class UdpMaster extends ModbusMaster {
+ private static final int MESSAGE_LENGTH = 1024;
+ private final IpParameters ipParameters;
+ private short nextTransactionId = 0;
+ // Runtime fields.
+ private BaseMessageParser messageParser;
+ private DatagramSocket socket;
+
+ /**
+ * Constructor for UdpMaster.
+ *
+ * Default to not validating the slave id in responses
+ *
+ * @param params a {@link IpParameters} object.
+ */
+ public UdpMaster(IpParameters params) {
+ this(params, false);
+ }
+
+ /**
+ *
Constructor for UdpMaster.
+ *
+ * @param params
+ * @param validateResponse - confirm that requested slave id is the same in the response
+ */
+ public UdpMaster(IpParameters params, boolean validateResponse) {
+ ipParameters = params;
+ this.validateResponse = validateResponse;
+ }
+
+ /**
+ * Getter for the field nextTransactionId.
+ *
+ * @return a short.
+ */
+ protected short getNextTransactionId() {
+ return nextTransactionId++;
+ }
+
+ @Override
+ public void init() throws ModbusInitException {
+ if (ipParameters.isEncapsulated())
+ messageParser = new EncapMessageParser(true);
+ else
+ messageParser = new XaMessageParser(true);
+
+ try {
+ socket = new DatagramSocket();
+ socket.setSoTimeout(getTimeout());
+ } catch (SocketException e) {
+ throw new ModbusInitException(e);
+ }
+ initialized = true;
+ }
+
+ @Override
+ public void destroy() {
+ socket.close();
+ initialized = false;
+ }
+
+ @Override
+ public ModbusResponse sendImpl(ModbusRequest request) throws ModbusTransportException {
+ // Wrap the modbus request in an ip request.
+ OutgoingRequestMessage ipRequest;
+ if (ipParameters.isEncapsulated())
+ ipRequest = new EncapMessageRequest(request);
+ else
+ ipRequest = new XaMessageRequest(request, getNextTransactionId());
+
+ IpMessageResponse ipResponse;
+
+ try {
+ int attempts = getRetries() + 1;
+
+ while (true) {
+ // Send the request.
+ sendImpl(ipRequest);
+
+ if (!ipRequest.expectsResponse())
+ return null;
+
+ // Receive the response.
+ try {
+ ipResponse = receiveImpl();
+ } catch (SocketTimeoutException e) {
+ attempts--;
+ if (attempts > 0)
+ // Try again.
+ continue;
+
+ throw new ModbusTransportException(e, request.getSlaveId());
+ }
+
+ // We got the response
+ break;
+ }
+
+ return ipResponse.getModbusResponse();
+ } catch (IOException e) {
+ throw new ModbusTransportException(e, request.getSlaveId());
+ }
+ }
+
+ private void sendImpl(OutgoingRequestMessage request) throws IOException {
+ byte[] data = request.getMessageData();
+ DatagramPacket packet = new DatagramPacket(data, data.length, InetAddress.getByName(ipParameters.getHost()),
+ ipParameters.getPort());
+ socket.send(packet);
+ }
+
+ private IpMessageResponse receiveImpl() throws IOException, ModbusTransportException {
+ DatagramPacket packet = new DatagramPacket(new byte[MESSAGE_LENGTH], MESSAGE_LENGTH);
+ socket.receive(packet);
+
+ // We could verify that the packet was received from the same address to which the request was sent,
+ // but let's not bother with that yet.
+
+ ByteQueue queue = new ByteQueue(packet.getData(), 0, packet.getLength());
+ IpMessageResponse response;
+ try {
+ response = (IpMessageResponse) messageParser.parseMessage(queue);
+ } catch (Exception e) {
+ throw new ModbusTransportException(e);
+ }
+
+ if (response == null)
+ throw new ModbusTransportException("Invalid response received");
+
+ return response;
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/udp/UdpSlave.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/udp/UdpSlave.java
new file mode 100644
index 0000000..4b1374a
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/udp/UdpSlave.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.udp;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ModbusSlaveSet;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.BaseMessageParser;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.BaseRequestHandler;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.ModbusUtils;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusInitException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.encap.EncapMessageParser;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.encap.EncapRequestHandler;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.xa.XaMessageParser;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.xa.XaRequestHandler;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.IncomingMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.IncomingRequestMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.OutgoingResponseMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * UdpSlave class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class UdpSlave extends ModbusSlaveSet {
+ final BaseMessageParser messageParser;
+ final BaseRequestHandler requestHandler;
+ // Configuration fields
+ private final int port;
+ private final ExecutorService executorService;
+ // Runtime fields.
+ DatagramSocket datagramSocket;
+
+ /**
+ * Constructor for UdpSlave.
+ *
+ * @param encapsulated a boolean.
+ */
+ public UdpSlave(boolean encapsulated) {
+ this(ModbusUtils.TCP_PORT, encapsulated);
+ }
+
+ /**
+ * Constructor for UdpSlave.
+ *
+ * @param port a int.
+ * @param encapsulated a boolean.
+ */
+ public UdpSlave(int port, boolean encapsulated) {
+ this.port = port;
+
+ if (encapsulated) {
+ messageParser = new EncapMessageParser(false);
+ requestHandler = new EncapRequestHandler(this);
+ } else {
+ messageParser = new XaMessageParser(false);
+ requestHandler = new XaRequestHandler(this);
+ }
+
+ executorService = Executors.newCachedThreadPool();
+ }
+
+ @Override
+ public void start() throws ModbusInitException {
+ try {
+ datagramSocket = new DatagramSocket(port);
+
+ DatagramPacket datagramPacket;
+ while (true) {
+ datagramPacket = new DatagramPacket(new byte[1028], 1028);
+ datagramSocket.receive(datagramPacket);
+
+ UdpConnectionHandler handler = new UdpConnectionHandler(datagramPacket);
+ executorService.execute(handler);
+ }
+ } catch (IOException e) {
+ throw new ModbusInitException(e);
+ }
+ }
+
+ @Override
+ public void stop() {
+ // Close the socket first to prevent new messages.
+ datagramSocket.close();
+
+ // Close the executor service.
+ executorService.shutdown();
+ try {
+ executorService.awaitTermination(3, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ getExceptionHandler().receivedException(e);
+ }
+ }
+
+ // int getSlaveId() {
+ // return slaveId;
+ // }
+ //
+ // ProcessImage getProcessImage() {
+ // return processImage;
+ // }
+
+ class UdpConnectionHandler implements Runnable {
+ private final DatagramPacket requestPacket;
+
+ UdpConnectionHandler(DatagramPacket requestPacket) {
+ this.requestPacket = requestPacket;
+ }
+
+ public void run() {
+ try {
+ ByteQueue requestQueue = new ByteQueue(requestPacket.getData(), 0, requestPacket.getLength());
+
+ // Parse the request data and get the response.
+ IncomingMessage request = messageParser.parseMessage(requestQueue);
+ OutgoingResponseMessage response = requestHandler.handleRequest((IncomingRequestMessage) request);
+
+ if (response == null)
+ return;
+
+ // Create a response packet.
+ byte[] responseData = response.getMessageData();
+ DatagramPacket responsePacket = new DatagramPacket(responseData, responseData.length,
+ requestPacket.getAddress(), requestPacket.getPort());
+
+ // Send the response back.
+ datagramSocket.send(responsePacket);
+ } catch (Exception e) {
+ getExceptionHandler().receivedException(e);
+ }
+ }
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/xa/XaMessage.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/xa/XaMessage.java
new file mode 100644
index 0000000..25bf990
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/xa/XaMessage.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.xa;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.ModbusUtils;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.IpMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * XaMessage class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class XaMessage extends IpMessage {
+ protected final int transactionId;
+
+ /**
+ * Constructor for XaMessage.
+ *
+ * @param modbusMessage a {@link ModbusMessage} object.
+ * @param transactionId a int.
+ */
+ public XaMessage(ModbusMessage modbusMessage, int transactionId) {
+ super(modbusMessage);
+ this.transactionId = transactionId;
+ }
+
+ /**
+ * getMessageData.
+ *
+ * @return an array of {@link byte} objects.
+ */
+ public byte[] getMessageData() {
+ ByteQueue msgQueue = new ByteQueue();
+
+ // Write the particular message.
+ modbusMessage.write(msgQueue);
+
+ // Create the XA message
+ ByteQueue xaQueue = new ByteQueue();
+ ModbusUtils.pushShort(xaQueue, transactionId);
+ ModbusUtils.pushShort(xaQueue, ModbusUtils.IP_PROTOCOL_ID);
+ ModbusUtils.pushShort(xaQueue, msgQueue.size());
+ xaQueue.push(msgQueue);
+
+ // Return the data.
+ return xaQueue.popAll();
+ }
+
+ /**
+ * Getter for the field transactionId.
+ *
+ * @return a int.
+ */
+ public int getTransactionId() {
+ return transactionId;
+ }
+
+ @Override
+ public ModbusMessage getModbusMessage() {
+ return modbusMessage;
+ }
+
+ @Override
+ public String toString() {
+ return "XaMessage [transactionId=" + transactionId + ", message=" + modbusMessage + "]";
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/xa/XaMessageParser.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/xa/XaMessageParser.java
new file mode 100644
index 0000000..c08c1c3
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/xa/XaMessageParser.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.xa;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.BaseMessageParser;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.IncomingMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * XaMessageParser class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class XaMessageParser extends BaseMessageParser {
+ /**
+ * Constructor for XaMessageParser.
+ *
+ * @param master a boolean.
+ */
+ public XaMessageParser(boolean master) {
+ super(master);
+ }
+
+ @Override
+ protected IncomingMessage parseMessageImpl(ByteQueue queue) throws Exception {
+ if (master)
+ return XaMessageResponse.createXaMessageResponse(queue);
+ return XaMessageRequest.createXaMessageRequest(queue);
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/xa/XaMessageRequest.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/xa/XaMessageRequest.java
new file mode 100644
index 0000000..6a58032
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/xa/XaMessageRequest.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.xa;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.ModbusUtils;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusRequest;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.IncomingRequestMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.OutgoingRequestMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * XaMessageRequest class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class XaMessageRequest extends XaMessage implements OutgoingRequestMessage, IncomingRequestMessage {
+ /**
+ * Constructor for XaMessageRequest.
+ *
+ * @param modbusRequest a {@link ModbusRequest} object.
+ * @param transactionId a int.
+ */
+ public XaMessageRequest(ModbusRequest modbusRequest, int transactionId) {
+ super(modbusRequest, transactionId);
+ }
+
+ static XaMessageRequest createXaMessageRequest(ByteQueue queue) throws ModbusTransportException {
+ // Remove the XA header
+ int transactionId = ModbusUtils.popShort(queue);
+ int protocolId = ModbusUtils.popShort(queue);
+ if (protocolId != ModbusUtils.IP_PROTOCOL_ID)
+ throw new ModbusTransportException("Unsupported IP protocol id: " + protocolId);
+ ModbusUtils.popShort(queue); // Length, which we don't care about.
+
+ // Create the modbus response.
+ ModbusRequest request = ModbusRequest.createModbusRequest(queue);
+ return new XaMessageRequest(request, transactionId);
+ }
+
+ @Override
+ public boolean expectsResponse() {
+ return modbusMessage.getSlaveId() != 0;
+ }
+
+ /**
+ * getModbusRequest.
+ *
+ * @return a {@link ModbusRequest} object.
+ */
+ public ModbusRequest getModbusRequest() {
+ return (ModbusRequest) modbusMessage;
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/xa/XaMessageResponse.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/xa/XaMessageResponse.java
new file mode 100644
index 0000000..78985e8
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/xa/XaMessageResponse.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.xa;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.ModbusUtils;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.IpMessageResponse;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusResponse;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * XaMessageResponse class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class XaMessageResponse extends XaMessage implements IpMessageResponse {
+ /**
+ * Constructor for XaMessageResponse.
+ *
+ * @param modbusResponse a {@link ModbusResponse} object.
+ * @param transactionId a int.
+ */
+ public XaMessageResponse(ModbusResponse modbusResponse, int transactionId) {
+ super(modbusResponse, transactionId);
+ }
+
+ static XaMessageResponse createXaMessageResponse(ByteQueue queue) throws ModbusTransportException {
+ // Remove the XA header
+ int transactionId = ModbusUtils.popShort(queue);
+ int protocolId = ModbusUtils.popShort(queue);
+ if (protocolId != ModbusUtils.IP_PROTOCOL_ID)
+ throw new ModbusTransportException("Unsupported IP protocol id: " + protocolId);
+ ModbusUtils.popShort(queue); // Length, which we don't care about.
+
+ // Create the modbus response.
+ ModbusResponse response = ModbusResponse.createModbusResponse(queue);
+ return new XaMessageResponse(response, transactionId);
+ }
+
+ /**
+ * getModbusResponse.
+ *
+ * @return a {@link ModbusResponse} object.
+ */
+ public ModbusResponse getModbusResponse() {
+ return (ModbusResponse) modbusMessage;
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/xa/XaRequestHandler.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/xa/XaRequestHandler.java
new file mode 100644
index 0000000..bcfce4b
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/xa/XaRequestHandler.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.xa;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ModbusSlaveSet;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.BaseRequestHandler;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusRequest;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusResponse;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.IncomingRequestMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.OutgoingResponseMessage;
+
+/**
+ * XaRequestHandler class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class XaRequestHandler extends BaseRequestHandler {
+ /**
+ * Constructor for XaRequestHandler.
+ *
+ * @param slave a {@link ModbusSlaveSet} object.
+ */
+ public XaRequestHandler(ModbusSlaveSet slave) {
+ super(slave);
+ }
+
+
+ public OutgoingResponseMessage handleRequest(IncomingRequestMessage req) throws Exception {
+ XaMessageRequest tcpRequest = (XaMessageRequest) req;
+ ModbusRequest request = tcpRequest.getModbusRequest();
+ ModbusResponse response = handleRequestImpl(request);
+ if (response == null)
+ return null;
+ return new XaMessageResponse(response, tcpRequest.transactionId);
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/xa/XaWaitingRoomKeyFactory.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/xa/XaWaitingRoomKeyFactory.java
new file mode 100644
index 0000000..6651179
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/ip/xa/XaWaitingRoomKeyFactory.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ip.xa;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.IncomingResponseMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.OutgoingRequestMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.WaitingRoomKey;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.WaitingRoomKeyFactory;
+
+/**
+ * XaWaitingRoomKeyFactory class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class XaWaitingRoomKeyFactory implements WaitingRoomKeyFactory {
+ @Override
+ public WaitingRoomKey createWaitingRoomKey(OutgoingRequestMessage request) {
+ return createWaitingRoomKey((XaMessage) request);
+ }
+
+ @Override
+ public WaitingRoomKey createWaitingRoomKey(IncomingResponseMessage response) {
+ return createWaitingRoomKey((XaMessage) response);
+ }
+
+ /**
+ * createWaitingRoomKey.
+ *
+ * @param msg a {@link XaMessage} object.
+ * @return a {@link WaitingRoomKey} object.
+ */
+ public WaitingRoomKey createWaitingRoomKey(XaMessage msg) {
+ return new XaWaitingRoomKey(msg.getTransactionId(), msg.getModbusMessage());
+ }
+
+ class XaWaitingRoomKey implements WaitingRoomKey {
+ private final int transactionId;
+ private final int slaveId;
+ private final byte functionCode;
+
+ public XaWaitingRoomKey(int transactionId, ModbusMessage msg) {
+ this.transactionId = transactionId;
+ this.slaveId = msg.getSlaveId();
+ this.functionCode = msg.getFunctionCode();
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + functionCode;
+ result = prime * result + slaveId;
+ result = prime * result + transactionId;
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ XaWaitingRoomKey other = (XaWaitingRoomKey) obj;
+ if (functionCode != other.functionCode)
+ return false;
+ if (slaveId != other.slaveId)
+ return false;
+ if (transactionId != other.transactionId)
+ return false;
+ return true;
+ }
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/locator/BaseLocator.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/locator/BaseLocator.java
new file mode 100644
index 0000000..51bfffc
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/locator/BaseLocator.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.locator;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.ModbusUtils;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.RangeAndOffset;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.DataType;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.RegisterRange;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusIdException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+
+import java.nio.charset.Charset;
+
+/**
+ * Abstract BaseLocator class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+abstract public class BaseLocator {
+ //
+ //
+ // Factory methods
+ //
+
+ protected final int range;
+ protected final int offset;
+ private final int slaveId;
+
+ /**
+ * Constructor for BaseLocator.
+ *
+ * @param slaveId a int.
+ * @param range a int.
+ * @param offset a int.
+ */
+ public BaseLocator(int slaveId, int range, int offset) {
+ this.slaveId = slaveId;
+ this.range = range;
+ this.offset = offset;
+ }
+
+ /**
+ * coilStatus.
+ *
+ * @param slaveId a int.
+ * @param offset a int.
+ * @return a {@link BaseLocator} object.
+ */
+ public static BaseLocator coilStatus(int slaveId, int offset) {
+ return new BinaryLocator(slaveId, RegisterRange.COIL_STATUS, offset);
+ }
+
+ /**
+ * inputStatus.
+ *
+ * @param slaveId a int.
+ * @param offset a int.
+ * @return a {@link BaseLocator} object.
+ */
+ public static BaseLocator inputStatus(int slaveId, int offset) {
+ return new BinaryLocator(slaveId, RegisterRange.INPUT_STATUS, offset);
+ }
+
+ /**
+ * inputRegister.
+ *
+ * @param slaveId a int.
+ * @param offset a int.
+ * @param dataType a int.
+ * @return a {@link BaseLocator} object.
+ */
+ public static BaseLocator inputRegister(int slaveId, int offset, int dataType) {
+ return new NumericLocator(slaveId, RegisterRange.INPUT_REGISTER, offset, dataType);
+ }
+
+ /**
+ * inputRegisterBit.
+ *
+ * @param slaveId a int.
+ * @param offset a int.
+ * @param bit a int.
+ * @return a {@link BaseLocator} object.
+ */
+ public static BaseLocator inputRegisterBit(int slaveId, int offset, int bit) {
+ return new BinaryLocator(slaveId, RegisterRange.INPUT_REGISTER, offset, bit);
+ }
+
+ /**
+ * holdingRegister.
+ *
+ * @param slaveId a int.
+ * @param offset a int.
+ * @param dataType a int.
+ * @return a {@link BaseLocator} object.
+ */
+ public static BaseLocator holdingRegister(int slaveId, int offset, int dataType) {
+ return new NumericLocator(slaveId, RegisterRange.HOLDING_REGISTER, offset, dataType);
+ }
+
+ /**
+ * holdingRegisterBit.
+ *
+ * @param slaveId a int.
+ * @param offset a int.
+ * @param bit a int.
+ * @return a {@link BaseLocator} object.
+ */
+ public static BaseLocator holdingRegisterBit(int slaveId, int offset, int bit) {
+ return new BinaryLocator(slaveId, RegisterRange.HOLDING_REGISTER, offset, bit);
+ }
+
+ /**
+ * createLocator.
+ *
+ * @param slaveId a int.
+ * @param registerId a int.
+ * @param dataType a int.
+ * @param bit a int.
+ * @param registerCount a int.
+ * @return a {@link BaseLocator} object.
+ */
+ public static BaseLocator> createLocator(int slaveId, int registerId, int dataType, int bit, int registerCount) {
+ RangeAndOffset rao = new RangeAndOffset(registerId);
+ return createLocator(slaveId, rao.getRange(), rao.getOffset(), dataType, bit, registerCount,
+ StringLocator.ASCII);
+ }
+
+ /**
+ * createLocator.
+ *
+ * @param slaveId a int.
+ * @param registerId a int.
+ * @param dataType a int.
+ * @param bit a int.
+ * @param registerCount a int.
+ * @param charset a {@link Charset} object.
+ * @return a {@link BaseLocator} object.
+ */
+ public static BaseLocator> createLocator(int slaveId, int registerId, int dataType, int bit, int registerCount,
+ Charset charset) {
+ RangeAndOffset rao = new RangeAndOffset(registerId);
+ return createLocator(slaveId, rao.getRange(), rao.getOffset(), dataType, bit, registerCount, charset);
+ }
+
+ /**
+ * createLocator.
+ *
+ * @param slaveId a int.
+ * @param range a int.
+ * @param offset a int.
+ * @param dataType a int.
+ * @param bit a int.
+ * @param registerCount a int.
+ * @return a {@link BaseLocator} object.
+ */
+ public static BaseLocator> createLocator(int slaveId, int range, int offset, int dataType, int bit,
+ int registerCount) {
+ return createLocator(slaveId, range, offset, dataType, bit, registerCount, StringLocator.ASCII);
+ }
+
+ /**
+ * createLocator.
+ *
+ * @param slaveId a int.
+ * @param range a int.
+ * @param offset a int.
+ * @param dataType a int.
+ * @param bit a int.
+ * @param registerCount a int.
+ * @param charset a {@link Charset} object.
+ * @return a {@link BaseLocator} object.
+ */
+ public static BaseLocator> createLocator(int slaveId, int range, int offset, int dataType, int bit,
+ int registerCount, Charset charset) {
+ if (dataType == DataType.BINARY) {
+ if (BinaryLocator.isBinaryRange(range))
+ return new BinaryLocator(slaveId, range, offset);
+ return new BinaryLocator(slaveId, range, offset, bit);
+ }
+ if (dataType == DataType.CHAR || dataType == DataType.VARCHAR)
+ return new StringLocator(slaveId, range, offset, dataType, registerCount, charset);
+ return new NumericLocator(slaveId, range, offset, dataType);
+ }
+
+ /**
+ * validate.
+ *
+ * @param registerCount a int.
+ */
+ protected void validate(int registerCount) {
+ try {
+ ModbusUtils.validateOffset(offset);
+ ModbusUtils.validateEndOffset(offset + registerCount - 1);
+ } catch (ModbusTransportException e) {
+ throw new ModbusIdException(e);
+ }
+ }
+
+ /**
+ * getDataType.
+ *
+ * @return a int.
+ */
+ abstract public int getDataType();
+
+ /**
+ * getRegisterCount.
+ *
+ * @return a int.
+ */
+ abstract public int getRegisterCount();
+
+ /**
+ * Getter for the field slaveId.
+ *
+ * @return a int.
+ */
+ public int getSlaveId() {
+ return slaveId;
+ }
+
+ /**
+ * Getter for the field range.
+ *
+ * @return a int.
+ */
+ public int getRange() {
+ return range;
+ }
+
+ /**
+ * Getter for the field offset.
+ *
+ * @return a int.
+ */
+ public int getOffset() {
+ return offset;
+ }
+
+ // public SlaveAndRange getSlaveAndRange() {
+ // return slaveAndRange;
+ // }
+
+ /**
+ * getEndOffset.
+ *
+ * @return a int.
+ */
+ public int getEndOffset() {
+ return offset + getRegisterCount() - 1;
+ }
+
+ /**
+ * bytesToValue.
+ *
+ * @param data an array of {@link byte} objects.
+ * @param requestOffset a int.
+ * @return a T object.
+ */
+ public T bytesToValue(byte[] data, int requestOffset) {
+ // Determined the offset normalized to the response data.
+ return bytesToValueRealOffset(data, offset - requestOffset);
+ }
+
+ /**
+ * bytesToValueRealOffset.
+ *
+ * @param data an array of {@link byte} objects.
+ * @param offset a int.
+ * @return a T object.
+ */
+ abstract public T bytesToValueRealOffset(byte[] data, int offset);
+
+ /**
+ * valueToShorts.
+ *
+ * @param value a T object.
+ * @return an array of {@link short} objects.
+ */
+ abstract public short[] valueToShorts(T value);
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/locator/BinaryLocator.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/locator/BinaryLocator.java
new file mode 100644
index 0000000..1b3afff
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/locator/BinaryLocator.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.locator;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.ModbusUtils;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.DataType;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.RegisterRange;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusIdException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.NotImplementedException;
+
+/**
+ * BinaryLocator class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class BinaryLocator extends BaseLocator {
+ private int bit = -1;
+
+ /**
+ * Constructor for BinaryLocator.
+ *
+ * @param slaveId a int.
+ * @param range a int.
+ * @param offset a int.
+ */
+ public BinaryLocator(int slaveId, int range, int offset) {
+ super(slaveId, range, offset);
+ if (!isBinaryRange(range))
+ throw new ModbusIdException("Non-bit requests can only be made from coil status and input status ranges");
+ validate();
+ }
+
+ /**
+ * Constructor for BinaryLocator.
+ *
+ * @param slaveId a int.
+ * @param range a int.
+ * @param offset a int.
+ * @param bit a int.
+ */
+ public BinaryLocator(int slaveId, int range, int offset, int bit) {
+ super(slaveId, range, offset);
+ if (isBinaryRange(range))
+ throw new ModbusIdException("Bit requests can only be made from holding registers and input registers");
+ this.bit = bit;
+ validate();
+ }
+
+ /**
+ * isBinaryRange.
+ *
+ * @param range a int.
+ * @return a boolean.
+ */
+ public static boolean isBinaryRange(int range) {
+ return range == RegisterRange.COIL_STATUS || range == RegisterRange.INPUT_STATUS;
+ }
+
+ /**
+ * validate.
+ */
+ protected void validate() {
+ super.validate(1);
+
+ if (!isBinaryRange(range))
+ ModbusUtils.validateBit(bit);
+ }
+
+ /**
+ * Getter for the field bit.
+ *
+ * @return a int.
+ */
+ public int getBit() {
+ return bit;
+ }
+
+
+ @Override
+ public int getDataType() {
+ return DataType.BINARY;
+ }
+
+
+ @Override
+ public int getRegisterCount() {
+ return 1;
+ }
+
+
+ @Override
+ public String toString() {
+ return "BinaryLocator(slaveId=" + getSlaveId() + ", range=" + range + ", offset=" + offset + ", bit=" + bit
+ + ")";
+ }
+
+
+ @Override
+ public Boolean bytesToValueRealOffset(byte[] data, int offset) {
+ // If this is a coil or input, convert to boolean.
+ if (range == RegisterRange.COIL_STATUS || range == RegisterRange.INPUT_STATUS)
+ return (((data[offset / 8] & 0xff) >> (offset % 8)) & 0x1) == 1;
+
+ // For the rest of the types, we double the normalized offset to account for short to byte.
+ offset *= 2;
+
+ // We could still be asking for a binary if it's a bit in a register.
+ return (((data[offset + 1 - bit / 8] & 0xff) >> (bit % 8)) & 0x1) == 1;
+ }
+
+
+ @Override
+ public short[] valueToShorts(Boolean value) {
+ throw new NotImplementedException();
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/locator/NumericLocator.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/locator/NumericLocator.java
new file mode 100644
index 0000000..278d2ba
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/locator/NumericLocator.java
@@ -0,0 +1,460 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.locator;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.DataType;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.RegisterRange;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.IllegalDataTypeException;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.math.RoundingMode;
+import java.util.Arrays;
+
+/**
+ * NumericLocator class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class NumericLocator extends BaseLocator {
+ private static final int[] DATA_TYPES = { //
+ DataType.TWO_BYTE_INT_UNSIGNED, //
+ DataType.TWO_BYTE_INT_SIGNED, //
+ DataType.TWO_BYTE_INT_UNSIGNED_SWAPPED, //
+ DataType.TWO_BYTE_INT_SIGNED_SWAPPED, //
+ DataType.FOUR_BYTE_INT_UNSIGNED, //
+ DataType.FOUR_BYTE_INT_SIGNED, //
+ DataType.FOUR_BYTE_INT_UNSIGNED_SWAPPED, //
+ DataType.FOUR_BYTE_INT_SIGNED_SWAPPED, //
+ DataType.FOUR_BYTE_INT_UNSIGNED_SWAPPED_SWAPPED, //
+ DataType.FOUR_BYTE_INT_SIGNED_SWAPPED_SWAPPED, //
+ DataType.FOUR_BYTE_FLOAT, //
+ DataType.FOUR_BYTE_FLOAT_SWAPPED, //
+ DataType.EIGHT_BYTE_INT_UNSIGNED, //
+ DataType.EIGHT_BYTE_INT_SIGNED, //
+ DataType.EIGHT_BYTE_INT_UNSIGNED_SWAPPED, //
+ DataType.EIGHT_BYTE_INT_SIGNED_SWAPPED, //
+ DataType.EIGHT_BYTE_FLOAT, //
+ DataType.EIGHT_BYTE_FLOAT_SWAPPED, //
+ DataType.TWO_BYTE_BCD, //
+ DataType.FOUR_BYTE_BCD, //
+ DataType.FOUR_BYTE_BCD_SWAPPED, //
+ DataType.FOUR_BYTE_MOD_10K, //
+ DataType.FOUR_BYTE_MOD_10K_SWAPPED, //
+ DataType.SIX_BYTE_MOD_10K,
+ DataType.SIX_BYTE_MOD_10K_SWAPPED,
+ DataType.EIGHT_BYTE_MOD_10K, //
+ DataType.EIGHT_BYTE_MOD_10K_SWAPPED, //
+ DataType.ONE_BYTE_INT_UNSIGNED_LOWER, //
+ DataType.ONE_BYTE_INT_UNSIGNED_UPPER
+ };
+
+ private final int dataType;
+ private RoundingMode roundingMode = RoundingMode.HALF_UP;
+
+ /**
+ * Constructor for NumericLocator.
+ *
+ * @param slaveId a int.
+ * @param range a int.
+ * @param offset a int.
+ * @param dataType a int.
+ */
+ public NumericLocator(int slaveId, int range, int offset, int dataType) {
+ super(slaveId, range, offset);
+ this.dataType = dataType;
+ validate();
+ }
+
+ private static void appendBCD(StringBuilder sb, byte b) {
+ sb.append(bcdNibbleToInt(b, true));
+ sb.append(bcdNibbleToInt(b, false));
+ }
+
+ private static int bcdNibbleToInt(byte b, boolean high) {
+ int n;
+ if (high)
+ n = (b >> 4) & 0xf;
+ else
+ n = b & 0xf;
+ if (n > 9)
+ n = 0;
+ return n;
+ }
+
+ private void validate() {
+ super.validate(getRegisterCount());
+
+ if (range == RegisterRange.COIL_STATUS || range == RegisterRange.INPUT_STATUS)
+ throw new IllegalDataTypeException("Only binary values can be read from Coil and Input ranges");
+ boolean b = Arrays.stream(DATA_TYPES).anyMatch(dt -> dt == dataType);
+ if (!b)
+ throw new IllegalDataTypeException("Invalid data type");
+ }
+
+ @Override
+ public int getDataType() {
+ return dataType;
+ }
+
+ /**
+ * Getter for the field roundingMode.
+ *
+ * @return a {@link RoundingMode} object.
+ */
+ public RoundingMode getRoundingMode() {
+ return roundingMode;
+ }
+
+ /**
+ * Setter for the field roundingMode.
+ *
+ * @param roundingMode a {@link RoundingMode} object.
+ */
+ public void setRoundingMode(RoundingMode roundingMode) {
+ this.roundingMode = roundingMode;
+ }
+
+ @Override
+ public String toString() {
+ return "NumericLocator(slaveId=" + getSlaveId() + ", range=" + range + ", offset=" + offset + ", dataType="
+ + dataType + ")";
+ }
+
+ @Override
+ public int getRegisterCount() {
+ switch (dataType) {
+ case DataType.TWO_BYTE_INT_UNSIGNED:
+ case DataType.TWO_BYTE_INT_SIGNED:
+ case DataType.TWO_BYTE_INT_UNSIGNED_SWAPPED:
+ case DataType.TWO_BYTE_INT_SIGNED_SWAPPED:
+ case DataType.TWO_BYTE_BCD:
+ case DataType.ONE_BYTE_INT_UNSIGNED_LOWER:
+ case DataType.ONE_BYTE_INT_UNSIGNED_UPPER:
+ return 1;
+ case DataType.FOUR_BYTE_INT_UNSIGNED:
+ case DataType.FOUR_BYTE_INT_SIGNED:
+ case DataType.FOUR_BYTE_INT_UNSIGNED_SWAPPED:
+ case DataType.FOUR_BYTE_INT_SIGNED_SWAPPED:
+ case DataType.FOUR_BYTE_INT_UNSIGNED_SWAPPED_SWAPPED:
+ case DataType.FOUR_BYTE_INT_SIGNED_SWAPPED_SWAPPED:
+ case DataType.FOUR_BYTE_FLOAT:
+ case DataType.FOUR_BYTE_FLOAT_SWAPPED:
+ case DataType.FOUR_BYTE_BCD:
+ case DataType.FOUR_BYTE_BCD_SWAPPED:
+ case DataType.FOUR_BYTE_MOD_10K:
+ case DataType.FOUR_BYTE_MOD_10K_SWAPPED:
+ return 2;
+ case DataType.SIX_BYTE_MOD_10K:
+ case DataType.SIX_BYTE_MOD_10K_SWAPPED:
+ return 3;
+ case DataType.EIGHT_BYTE_INT_UNSIGNED:
+ case DataType.EIGHT_BYTE_INT_SIGNED:
+ case DataType.EIGHT_BYTE_INT_UNSIGNED_SWAPPED:
+ case DataType.EIGHT_BYTE_INT_SIGNED_SWAPPED:
+ case DataType.EIGHT_BYTE_FLOAT:
+ case DataType.EIGHT_BYTE_FLOAT_SWAPPED:
+ case DataType.EIGHT_BYTE_MOD_10K:
+ case DataType.EIGHT_BYTE_MOD_10K_SWAPPED:
+ return 4;
+ }
+
+ throw new RuntimeException("Unsupported data type: " + dataType);
+ }
+
+ @Override
+ public Number bytesToValueRealOffset(byte[] data, int offset) {
+ offset *= 2;
+
+ // 2 bytes
+ if (dataType == DataType.TWO_BYTE_INT_UNSIGNED)
+ return ((data[offset] & 0xff) << 8) | (data[offset + 1] & 0xff);
+
+ if (dataType == DataType.TWO_BYTE_INT_SIGNED)
+ return (short) (((data[offset] & 0xff) << 8) | (data[offset + 1] & 0xff));
+
+ if (dataType == DataType.TWO_BYTE_INT_UNSIGNED_SWAPPED)
+ return ((data[offset + 1] & 0xff) << 8) | (data[offset] & 0xff);
+
+ if (dataType == DataType.TWO_BYTE_INT_SIGNED_SWAPPED)
+ return (short) (((data[offset + 1] & 0xff) << 8) | (data[offset] & 0xff));
+
+ if (dataType == DataType.TWO_BYTE_BCD) {
+ StringBuilder sb = new StringBuilder();
+ appendBCD(sb, data[offset]);
+ appendBCD(sb, data[offset + 1]);
+ return Short.parseShort(sb.toString());
+ }
+
+ // 1 byte
+ if (dataType == DataType.ONE_BYTE_INT_UNSIGNED_LOWER)
+ return data[offset + 1] & 0xff;
+ if (dataType == DataType.ONE_BYTE_INT_UNSIGNED_UPPER)
+ return data[offset] & 0xff;
+
+ // 4 bytes
+ if (dataType == DataType.FOUR_BYTE_INT_UNSIGNED)
+ return (long) ((data[offset] & 0xff)) << 24 | ((long) ((data[offset + 1] & 0xff)) << 16)
+ | ((long) ((data[offset + 2] & 0xff)) << 8) | ((data[offset + 3] & 0xff));
+
+ if (dataType == DataType.FOUR_BYTE_INT_SIGNED)
+ return ((data[offset] & 0xff) << 24) | ((data[offset + 1] & 0xff) << 16)
+ | ((data[offset + 2] & 0xff) << 8) | (data[offset + 3] & 0xff);
+
+ if (dataType == DataType.FOUR_BYTE_INT_UNSIGNED_SWAPPED)
+ return ((long) ((data[offset + 2] & 0xff)) << 24) | ((long) ((data[offset + 3] & 0xff)) << 16)
+ | ((long) ((data[offset] & 0xff)) << 8) | ((data[offset + 1] & 0xff));
+
+ if (dataType == DataType.FOUR_BYTE_INT_SIGNED_SWAPPED)
+ return ((data[offset + 2] & 0xff) << 24) | ((data[offset + 3] & 0xff) << 16)
+ | ((data[offset] & 0xff) << 8) | (data[offset + 1] & 0xff);
+
+ if (dataType == DataType.FOUR_BYTE_INT_UNSIGNED_SWAPPED_SWAPPED)
+ return ((long) ((data[offset + 3] & 0xff)) << 24) | (((data[offset + 2] & 0xff) << 16))
+ | ((long) ((data[offset + 1] & 0xff)) << 8) | (data[offset] & 0xff);
+
+ if (dataType == DataType.FOUR_BYTE_INT_SIGNED_SWAPPED_SWAPPED)
+ return ((data[offset + 3] & 0xff) << 24) | ((data[offset + 2] & 0xff) << 16)
+ | ((data[offset + 1] & 0xff) << 8) | ((data[offset] & 0xff));
+
+ if (dataType == DataType.FOUR_BYTE_FLOAT)
+ return Float.intBitsToFloat(((data[offset] & 0xff) << 24) | ((data[offset + 1] & 0xff) << 16)
+ | ((data[offset + 2] & 0xff) << 8) | (data[offset + 3] & 0xff));
+
+ if (dataType == DataType.FOUR_BYTE_FLOAT_SWAPPED)
+ return Float.intBitsToFloat(((data[offset + 2] & 0xff) << 24) | ((data[offset + 3] & 0xff) << 16)
+ | ((data[offset] & 0xff) << 8) | (data[offset + 1] & 0xff));
+
+ if (dataType == DataType.FOUR_BYTE_BCD) {
+ StringBuilder sb = new StringBuilder();
+ appendBCD(sb, data[offset]);
+ appendBCD(sb, data[offset + 1]);
+ appendBCD(sb, data[offset + 2]);
+ appendBCD(sb, data[offset + 3]);
+ return Integer.parseInt(sb.toString());
+ }
+
+ if (dataType == DataType.FOUR_BYTE_BCD_SWAPPED) {
+ StringBuilder sb = new StringBuilder();
+ appendBCD(sb, data[offset + 2]);
+ appendBCD(sb, data[offset + 3]);
+ appendBCD(sb, data[offset]);
+ appendBCD(sb, data[offset + 1]);
+ return Integer.parseInt(sb.toString());
+ }
+
+ //MOD10K types
+ if (dataType == DataType.FOUR_BYTE_MOD_10K_SWAPPED)
+ return BigInteger.valueOf((((data[offset + 2] & 0xff) << 8) + (data[offset + 3] & 0xff))).multiply(BigInteger.valueOf(10000L))
+ .add(BigInteger.valueOf((((data[offset] & 0xff) << 8) + (data[offset + 1] & 0xff))));
+ if (dataType == DataType.FOUR_BYTE_MOD_10K)
+ return BigInteger.valueOf((((data[offset] & 0xff) << 8) + (data[offset + 1] & 0xff))).multiply(BigInteger.valueOf(10000L))
+ .add(BigInteger.valueOf((((data[offset + 2] & 0xff) << 8) + (data[offset + 3] & 0xff))));
+ if (dataType == DataType.SIX_BYTE_MOD_10K_SWAPPED)
+ return BigInteger.valueOf((((data[offset + 4] & 0xff) << 8) + (data[offset + 5] & 0xff))).multiply(BigInteger.valueOf(100000000L))
+ .add(BigInteger.valueOf((((data[offset + 2] & 0xff) << 8) + (data[offset + 3] & 0xff))).multiply(BigInteger.valueOf(10000L)))
+ .add(BigInteger.valueOf((((data[offset] & 0xff) << 8) + (data[offset + 1] & 0xff))));
+ if (dataType == DataType.SIX_BYTE_MOD_10K)
+ return BigInteger.valueOf((((data[offset] & 0xff) << 8) + (data[offset + 1] & 0xff))).multiply(BigInteger.valueOf(100000000L))
+ .add(BigInteger.valueOf((((data[offset + 2] & 0xff) << 8) + (data[offset + 3] & 0xff))).multiply(BigInteger.valueOf(10000L)))
+ .add(BigInteger.valueOf((((data[offset + 4] & 0xff) << 8) + (data[offset + 5] & 0xff))));
+ if (dataType == DataType.EIGHT_BYTE_MOD_10K_SWAPPED)
+ return BigInteger.valueOf((((data[offset + 6] & 0xff) << 8) + (data[offset + 7] & 0xff))).multiply(BigInteger.valueOf(1000000000000L))
+ .add(BigInteger.valueOf((((data[offset + 4] & 0xff) << 8) + (data[offset + 5] & 0xff))).multiply(BigInteger.valueOf(100000000L)))
+ .add(BigInteger.valueOf((((data[offset + 2] & 0xff) << 8) + (data[offset + 3] & 0xff))).multiply(BigInteger.valueOf(10000L)))
+ .add(BigInteger.valueOf((((data[offset] & 0xff) << 8) + (data[offset + 1] & 0xff))));
+ if (dataType == DataType.EIGHT_BYTE_MOD_10K)
+ return BigInteger.valueOf((((data[offset] & 0xff) << 8) + (data[offset + 1] & 0xff))).multiply(BigInteger.valueOf(1000000000000L))
+ .add(BigInteger.valueOf((((data[offset + 2] & 0xff) << 8) + (data[offset + 3] & 0xff))).multiply(BigInteger.valueOf(100000000L)))
+ .add(BigInteger.valueOf((((data[offset + 4] & 0xff) << 8) + (data[offset + 5] & 0xff))).multiply(BigInteger.valueOf(10000L)))
+ .add(BigInteger.valueOf((((data[offset + 6] & 0xff) << 8) + (data[offset + 7] & 0xff))));
+
+ // 8 bytes
+ if (dataType == DataType.EIGHT_BYTE_INT_UNSIGNED) {
+ byte[] b9 = new byte[9];
+ System.arraycopy(data, offset, b9, 1, 8);
+ return new BigInteger(b9);
+ }
+
+ if (dataType == DataType.EIGHT_BYTE_INT_SIGNED)
+ return ((long) ((data[offset] & 0xff)) << 56) | ((long) ((data[offset + 1] & 0xff)) << 48)
+ | ((long) ((data[offset + 2] & 0xff)) << 40) | ((long) ((data[offset + 3] & 0xff)) << 32)
+ | ((long) ((data[offset + 4] & 0xff)) << 24) | ((long) ((data[offset + 5] & 0xff)) << 16)
+ | ((long) ((data[offset + 6] & 0xff)) << 8) | ((data[offset + 7] & 0xff));
+
+ if (dataType == DataType.EIGHT_BYTE_INT_UNSIGNED_SWAPPED) {
+ byte[] b9 = new byte[9];
+ b9[1] = data[offset + 6];
+ b9[2] = data[offset + 7];
+ b9[3] = data[offset + 4];
+ b9[4] = data[offset + 5];
+ b9[5] = data[offset + 2];
+ b9[6] = data[offset + 3];
+ b9[7] = data[offset];
+ b9[8] = data[offset + 1];
+ return new BigInteger(b9);
+ }
+
+ if (dataType == DataType.EIGHT_BYTE_INT_SIGNED_SWAPPED)
+ return ((long) ((data[offset + 6] & 0xff)) << 56) | ((long) ((data[offset + 7] & 0xff)) << 48)
+ | ((long) ((data[offset + 4] & 0xff)) << 40) | ((long) ((data[offset + 5] & 0xff)) << 32)
+ | ((long) ((data[offset + 2] & 0xff)) << 24) | ((long) ((data[offset + 3] & 0xff)) << 16)
+ | ((long) ((data[offset] & 0xff)) << 8) | ((data[offset + 1] & 0xff));
+
+ if (dataType == DataType.EIGHT_BYTE_FLOAT)
+ return Double.longBitsToDouble(((long) ((data[offset] & 0xff)) << 56)
+ | ((long) ((data[offset + 1] & 0xff)) << 48) | ((long) ((data[offset + 2] & 0xff)) << 40)
+ | ((long) ((data[offset + 3] & 0xff)) << 32) | ((long) ((data[offset + 4] & 0xff)) << 24)
+ | ((long) ((data[offset + 5] & 0xff)) << 16) | ((long) ((data[offset + 6] & 0xff)) << 8)
+ | ((data[offset + 7] & 0xff)));
+
+ if (dataType == DataType.EIGHT_BYTE_FLOAT_SWAPPED)
+ return Double.longBitsToDouble(((long) ((data[offset + 6] & 0xff)) << 56)
+ | ((long) ((data[offset + 7] & 0xff)) << 48) | ((long) ((data[offset + 4] & 0xff)) << 40)
+ | ((long) ((data[offset + 5] & 0xff)) << 32) | ((long) ((data[offset + 2] & 0xff)) << 24)
+ | ((long) ((data[offset + 3] & 0xff)) << 16) | ((long) ((data[offset] & 0xff)) << 8)
+ | ((data[offset + 1] & 0xff)));
+
+ throw new RuntimeException("Unsupported data type: " + dataType);
+ }
+
+ @Override
+ public short[] valueToShorts(Number value) {
+ // 2 bytes
+ if (dataType == DataType.TWO_BYTE_INT_UNSIGNED || dataType == DataType.TWO_BYTE_INT_SIGNED)
+ return new short[]{toShort(value)};
+
+ if (dataType == DataType.TWO_BYTE_INT_SIGNED_SWAPPED || dataType == DataType.TWO_BYTE_INT_UNSIGNED_SWAPPED) {
+ short sval = toShort(value);
+ //0x1100
+ return new short[]{(short) (((sval & 0xFF00) >> 8) | ((sval & 0x00FF) << 8))};
+ }
+
+ if (dataType == DataType.TWO_BYTE_BCD) {
+ short s = toShort(value);
+ return new short[]{(short) ((((s / 1000) % 10) << 12) | (((s / 100) % 10) << 8) | (((s / 10) % 10) << 4) | (s % 10))};
+ }
+
+ if (dataType == DataType.ONE_BYTE_INT_UNSIGNED_LOWER) {
+ return new short[]{(short) (toShort(value) & 0x00FF)};
+ }
+ if (dataType == DataType.ONE_BYTE_INT_UNSIGNED_UPPER) {
+ return new short[]{(short) ((toShort(value) << 8) & 0xFF00)};
+ }
+
+ // 4 bytes
+ if (dataType == DataType.FOUR_BYTE_INT_UNSIGNED || dataType == DataType.FOUR_BYTE_INT_SIGNED) {
+ int i = toInt(value);
+ return new short[]{(short) (i >> 16), (short) i};
+ }
+
+ if (dataType == DataType.FOUR_BYTE_INT_UNSIGNED_SWAPPED || dataType == DataType.FOUR_BYTE_INT_SIGNED_SWAPPED) {
+ int i = toInt(value);
+ return new short[]{(short) i, (short) (i >> 16)};
+ }
+
+ if (dataType == DataType.FOUR_BYTE_INT_SIGNED_SWAPPED_SWAPPED
+ || dataType == DataType.FOUR_BYTE_INT_UNSIGNED_SWAPPED_SWAPPED) {
+ int i = toInt(value);
+ short topWord = (short) (((i & 0xFF) << 8) | ((i >> 8) & 0xFF));
+ short bottomWord = (short) (((i >> 24) & 0x000000FF) | ((i >> 8) & 0x0000FF00));
+ return new short[]{topWord, bottomWord};
+ }
+
+ if (dataType == DataType.FOUR_BYTE_FLOAT) {
+ int i = Float.floatToIntBits(value.floatValue());
+ return new short[]{(short) (i >> 16), (short) i};
+ }
+
+ if (dataType == DataType.FOUR_BYTE_FLOAT_SWAPPED) {
+ int i = Float.floatToIntBits(value.floatValue());
+ return new short[]{(short) i, (short) (i >> 16)};
+ }
+
+ if (dataType == DataType.FOUR_BYTE_BCD) {
+ int i = toInt(value);
+ return new short[]{
+ (short) ((((i / 10000000) % 10) << 12) | (((i / 1000000) % 10) << 8) | (((i / 100000) % 10) << 4) | ((i / 10000) % 10)),
+ (short) ((((i / 1000) % 10) << 12) | (((i / 100) % 10) << 8) | (((i / 10) % 10) << 4) | (i % 10))};
+ }
+
+ // MOD10K
+ if (dataType == DataType.FOUR_BYTE_MOD_10K) {
+ long l = value.longValue();
+ return new short[]{(short) ((l / 10000) % 10000), (short) (l % 10000)};
+ }
+ if (dataType == DataType.FOUR_BYTE_MOD_10K_SWAPPED) {
+ long l = value.longValue();
+ return new short[]{(short) (l % 10000), (short) ((l / 10000) % 10000)};
+ }
+ if (dataType == DataType.SIX_BYTE_MOD_10K) {
+ long l = value.longValue();
+ return new short[]{(short) ((l / 100000000L) % 10000), (short) ((l / 10000) % 10000), (short) (l % 10000)};
+ }
+ if (dataType == DataType.SIX_BYTE_MOD_10K_SWAPPED) {
+ long l = value.longValue();
+ return new short[]{(short) (l % 10000), (short) ((l / 10000) % 10000), (short) ((l / 100000000L) % 10000)};
+ }
+ if (dataType == DataType.EIGHT_BYTE_MOD_10K) {
+ long l = value.longValue();
+ return new short[]{(short) ((l / 1000000000000L) % 10000), (short) ((l / 100000000L) % 10000), (short) ((l / 10000) % 10000), (short) (l % 10000)};
+ }
+ if (dataType == DataType.EIGHT_BYTE_MOD_10K_SWAPPED) {
+ long l = value.longValue();
+ return new short[]{(short) (l % 10000), (short) ((l / 10000) % 10000), (short) ((l / 100000000L) % 10000), (short) ((l / 1000000000000L) % 10000)};
+ }
+
+ // 8 bytes
+ if (dataType == DataType.EIGHT_BYTE_INT_UNSIGNED || dataType == DataType.EIGHT_BYTE_INT_SIGNED) {
+ long l = value.longValue();
+ return new short[]{(short) (l >> 48), (short) (l >> 32), (short) (l >> 16), (short) l};
+ }
+
+ if (dataType == DataType.EIGHT_BYTE_INT_UNSIGNED_SWAPPED || dataType == DataType.EIGHT_BYTE_INT_SIGNED_SWAPPED) {
+ long l = value.longValue();
+ return new short[]{(short) l, (short) (l >> 16), (short) (l >> 32), (short) (l >> 48)};
+ }
+
+ if (dataType == DataType.EIGHT_BYTE_FLOAT) {
+ long l = Double.doubleToLongBits(value.doubleValue());
+ return new short[]{(short) (l >> 48), (short) (l >> 32), (short) (l >> 16), (short) l};
+ }
+
+ if (dataType == DataType.EIGHT_BYTE_FLOAT_SWAPPED) {
+ long l = Double.doubleToLongBits(value.doubleValue());
+ return new short[]{(short) l, (short) (l >> 16), (short) (l >> 32), (short) (l >> 48)};
+ }
+
+ throw new RuntimeException("Unsupported data type: " + dataType);
+ }
+
+ private short toShort(Number value) {
+ return (short) toInt(value);
+ }
+
+ private int toInt(Number value) {
+ if (value instanceof Double)
+ return new BigDecimal(value.doubleValue()).setScale(0, roundingMode).intValue();
+ if (value instanceof Float)
+ return new BigDecimal(value.floatValue()).setScale(0, roundingMode).intValue();
+ if (value instanceof BigDecimal)
+ return ((BigDecimal) value).setScale(0, roundingMode).intValue();
+ return value.intValue();
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/locator/StringLocator.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/locator/StringLocator.java
new file mode 100644
index 0000000..5c797b3
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/locator/StringLocator.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.locator;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.DataType;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.RegisterRange;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.IllegalDataTypeException;
+
+import java.nio.charset.Charset;
+
+/**
+ * StringLocator class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class StringLocator extends BaseLocator {
+ /**
+ * Constant ASCII
+ */
+ public static final Charset ASCII = Charset.forName("ASCII");
+
+ private final int dataType;
+ private final int registerCount;
+ private final Charset charset;
+
+ /**
+ * Constructor for StringLocator.
+ *
+ * @param slaveId a int.
+ * @param range a int.
+ * @param offset a int.
+ * @param dataType a int.
+ * @param registerCount a int.
+ */
+ public StringLocator(int slaveId, int range, int offset, int dataType, int registerCount) {
+ this(slaveId, range, offset, dataType, registerCount, ASCII);
+ }
+
+ /**
+ * Constructor for StringLocator.
+ *
+ * @param slaveId a int.
+ * @param range a int.
+ * @param offset a int.
+ * @param dataType a int.
+ * @param registerCount a int.
+ * @param charset a {@link Charset} object.
+ */
+ public StringLocator(int slaveId, int range, int offset, int dataType, int registerCount, Charset charset) {
+ super(slaveId, range, offset);
+ this.dataType = dataType;
+ this.registerCount = registerCount;
+ this.charset = charset;
+ validate();
+ }
+
+ private void validate() {
+ super.validate(registerCount);
+
+ if (range == RegisterRange.COIL_STATUS || range == RegisterRange.INPUT_STATUS)
+ throw new IllegalDataTypeException("Only binary values can be read from Coil and Input ranges");
+
+ if (dataType != DataType.CHAR && dataType != DataType.VARCHAR)
+ throw new IllegalDataTypeException("Invalid data type");
+ }
+
+
+ @Override
+ public int getDataType() {
+ return dataType;
+ }
+
+
+ @Override
+ public int getRegisterCount() {
+ return registerCount;
+ }
+
+
+ @Override
+ public String toString() {
+ return "StringLocator(slaveId=" + getSlaveId() + ", range=" + range + ", offset=" + offset + ", dataType="
+ + dataType + ", registerCount=" + registerCount + ", charset=" + charset + ")";
+ }
+
+
+ @Override
+ public String bytesToValueRealOffset(byte[] data, int offset) {
+ offset *= 2;
+ int length = registerCount * 2;
+
+ if (dataType == DataType.CHAR)
+ return new String(data, offset, length, charset);
+
+ if (dataType == DataType.VARCHAR) {
+ int nullPos = -1;
+ for (int i = offset; i < offset + length; i++) {
+ if (data[i] == 0) {
+ nullPos = i;
+ break;
+ }
+ }
+
+ if (nullPos == -1)
+ return new String(data, offset, length, charset);
+ return new String(data, offset, nullPos, charset);
+ }
+
+ throw new RuntimeException("Unsupported data type: " + dataType);
+ }
+
+
+ @Override
+ public short[] valueToShorts(String value) {
+ short[] result = new short[registerCount];
+ int resultByteLen = registerCount * 2;
+
+ int length;
+ if (value != null) {
+ byte[] bytes = value.getBytes(charset);
+
+ length = resultByteLen;
+ if (length > bytes.length)
+ length = bytes.length;
+
+ for (int i = 0; i < length; i++)
+ setByte(result, i, bytes[i] & 0xff);
+ } else
+ length = 0;
+
+ if (dataType == DataType.CHAR) {
+ // Pad the rest with spaces
+ for (int i = length; i < resultByteLen; i++)
+ setByte(result, i, 0x20);
+ } else if (dataType == DataType.VARCHAR) {
+ if (length >= resultByteLen)
+ // Ensure the last byte is a null terminator.
+ result[registerCount - 1] &= 0xff00;
+ else {
+ // Pad the rest with null.
+ for (int i = length; i < resultByteLen; i++)
+ setByte(result, i, 0);
+ }
+ } else
+ throw new RuntimeException("Unsupported data type: " + dataType);
+
+ return result;
+ }
+
+ private void setByte(short[] s, int byteIndex, int value) {
+ if (byteIndex % 2 == 0)
+ s[byteIndex / 2] |= value << 8;
+ else
+ s[byteIndex / 2] |= value;
+ }
+ //
+ // public static void main(String[] args) {
+ // StringLocator l1 = new StringLocator(1, RegisterRange.HOLDING_REGISTER, 0, DataType.CHAR, 4);
+ // StringLocator l2 = new StringLocator(1, RegisterRange.HOLDING_REGISTER, 0, DataType.VARCHAR, 4);
+ //
+ // short[] s;
+ //
+ // s = l1.valueToShorts("abcdefg");
+ // System.out.println(new String(l1.bytesToValue(toBytes(s), 0)));
+ //
+ // s = l1.valueToShorts("abcdefgh");
+ // System.out.println(new String(l1.bytesToValue(toBytes(s), 0)));
+ //
+ // s = l1.valueToShorts("abcdefghi");
+ // System.out.println(new String(l1.bytesToValue(toBytes(s), 0)));
+ //
+ // s = l2.valueToShorts("abcdef");
+ // System.out.println(new String(l2.bytesToValue(toBytes(s), 0)));
+ //
+ // s = l2.valueToShorts("abcdefg");
+ // System.out.println(new String(l2.bytesToValue(toBytes(s), 0)));
+ //
+ // s = l2.valueToShorts("abcdefgh");
+ // System.out.println(new String(l2.bytesToValue(toBytes(s), 0)));
+ //
+ // s = l2.valueToShorts("abcdefghi");
+ // System.out.println(new String(l2.bytesToValue(toBytes(s), 0)));
+ // }
+ //
+ // private static byte[] toBytes(short[] s) {
+ // byte[] b = new byte[s.length * 2];
+ // for (int i = 0; i < s.length; i++) {
+ // b[i * 2] = (byte) ((s[i] >> 8) & 0xff);
+ // b[i * 2 + 1] = (byte) (s[i] & 0xff);
+ // }
+ // return b;
+ // }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ExceptionRequest.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ExceptionRequest.java
new file mode 100644
index 0000000..36fffcf
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ExceptionRequest.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.Modbus;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ProcessImage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.ShouldNeverHappenException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * ExceptionRequest class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class ExceptionRequest extends ModbusRequest {
+ private final byte functionCode;
+ private final byte exceptionCode;
+
+ /**
+ * Constructor for ExceptionRequest.
+ *
+ * @param slaveId a int.
+ * @param functionCode a byte.
+ * @param exceptionCode a byte.
+ * @throws ModbusTransportException if any.
+ */
+ public ExceptionRequest(int slaveId, byte functionCode, byte exceptionCode) throws ModbusTransportException {
+ super(slaveId);
+ this.functionCode = functionCode;
+ this.exceptionCode = exceptionCode;
+ }
+
+ @Override
+ public void validate(Modbus modbus) {
+ // no op
+ }
+
+ @Override
+ protected void writeRequest(ByteQueue queue) {
+ throw new ShouldNeverHappenException("wha");
+ }
+
+ @Override
+ protected void readRequest(ByteQueue queue) {
+ queue.clear();
+ }
+
+ @Override
+ ModbusResponse getResponseInstance(int slaveId) throws ModbusTransportException {
+ return new ExceptionResponse(slaveId, functionCode, exceptionCode);
+ }
+
+ @Override
+ ModbusResponse handleImpl(ProcessImage processImage) throws ModbusTransportException {
+ return getResponseInstance(slaveId);
+ }
+
+ @Override
+ public byte getFunctionCode() {
+ return functionCode;
+ }
+
+ /**
+ * Getter for the field exceptionCode.
+ *
+ * @return a byte.
+ */
+ public byte getExceptionCode() {
+ return exceptionCode;
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ExceptionResponse.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ExceptionResponse.java
new file mode 100644
index 0000000..045f1d3
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ExceptionResponse.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * ExceptionResponse class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class ExceptionResponse extends ModbusResponse {
+ private final byte functionCode;
+
+ /**
+ * Constructor for ExceptionResponse.
+ *
+ * @param slaveId a int.
+ * @param functionCode a byte.
+ * @param exceptionCode a byte.
+ * @throws ModbusTransportException if any.
+ */
+ public ExceptionResponse(int slaveId, byte functionCode, byte exceptionCode) throws ModbusTransportException {
+ super(slaveId);
+ this.functionCode = functionCode;
+ setException(exceptionCode);
+ }
+
+ @Override
+ public byte getFunctionCode() {
+ return functionCode;
+ }
+
+ @Override
+ protected void readResponse(ByteQueue queue) {
+ // no op
+ }
+
+ @Override
+ protected void writeResponse(ByteQueue queue) {
+ // no op
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ModbusMessage.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ModbusMessage.java
new file mode 100644
index 0000000..d600ff1
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ModbusMessage.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.ModbusUtils;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * Abstract ModbusMessage class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+abstract public class ModbusMessage {
+ protected int slaveId;
+
+ /**
+ * Constructor for ModbusMessage.
+ *
+ * @param slaveId a int.
+ * @throws ModbusTransportException if any.
+ */
+ public ModbusMessage(int slaveId) throws ModbusTransportException {
+ // Validate the node id. Note that a 0 slave id is a broadcast message.
+ if (slaveId < 0 /* || slaveId > 247 */)
+ throw new ModbusTransportException("Invalid slave id", slaveId);
+
+ this.slaveId = slaveId;
+ }
+
+ /**
+ * Getter for the field slaveId.
+ *
+ * @return a int.
+ */
+ public int getSlaveId() {
+ return slaveId;
+ }
+
+ /**
+ * getFunctionCode.
+ *
+ * @return a byte.
+ */
+ abstract public byte getFunctionCode();
+
+ /**
+ * write.
+ *
+ * @param queue a {@link ByteQueue} object.
+ */
+ final public void write(ByteQueue queue) {
+ ModbusUtils.pushByte(queue, slaveId);
+ writeImpl(queue);
+ }
+
+ /**
+ * writeImpl.
+ *
+ * @param queue a {@link ByteQueue} object.
+ */
+ abstract protected void writeImpl(ByteQueue queue);
+
+ /**
+ * convertToBytes.
+ *
+ * @param bdata an array of {@link boolean} objects.
+ * @return an array of {@link byte} objects.
+ */
+ protected byte[] convertToBytes(boolean[] bdata) {
+ int byteCount = (bdata.length + 7) / 8;
+ byte[] data = new byte[byteCount];
+ for (int i = 0; i < bdata.length; i++)
+ data[i / 8] |= (bdata[i] ? 1 : 0) << (i % 8);
+ return data;
+ }
+
+ /**
+ * convertToBytes.
+ *
+ * @param sdata an array of {@link short} objects.
+ * @return an array of {@link byte} objects.
+ */
+ protected byte[] convertToBytes(short[] sdata) {
+ int byteCount = sdata.length * 2;
+ byte[] data = new byte[byteCount];
+ for (int i = 0; i < sdata.length; i++) {
+ data[i * 2] = (byte) (0xff & (sdata[i] >> 8));
+ data[i * 2 + 1] = (byte) (0xff & sdata[i]);
+ }
+ return data;
+ }
+
+ /**
+ * convertToBooleans.
+ *
+ * @param data an array of {@link byte} objects.
+ * @return an array of {@link boolean} objects.
+ */
+ protected boolean[] convertToBooleans(byte[] data) {
+ boolean[] bdata = new boolean[data.length * 8];
+ for (int i = 0; i < bdata.length; i++)
+ bdata[i] = ((data[i / 8] >> (i % 8)) & 0x1) == 1;
+ return bdata;
+ }
+
+ /**
+ * convertToShorts.
+ *
+ * @param data an array of {@link byte} objects.
+ * @return an array of {@link short} objects.
+ */
+ protected short[] convertToShorts(byte[] data) {
+ short[] sdata = new short[data.length / 2];
+ for (int i = 0; i < sdata.length; i++)
+ sdata[i] = ModbusUtils.toShort(data[i * 2], data[i * 2 + 1]);
+ return sdata;
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ModbusRequest.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ModbusRequest.java
new file mode 100644
index 0000000..5e381cb
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ModbusRequest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.Modbus;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ProcessImage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.ModbusUtils;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.ExceptionCode;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.FunctionCode;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.IllegalDataAddressException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * Abstract ModbusRequest class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+abstract public class ModbusRequest extends ModbusMessage {
+ ModbusRequest(int slaveId) throws ModbusTransportException {
+ super(slaveId);
+ }
+
+ /**
+ * createModbusRequest.
+ *
+ * @param queue a {@link ByteQueue} object.
+ * @return a {@link ModbusRequest} object.
+ * @throws ModbusTransportException if any.
+ */
+ public static ModbusRequest createModbusRequest(ByteQueue queue) throws ModbusTransportException {
+ int slaveId = ModbusUtils.popUnsignedByte(queue);
+ byte functionCode = queue.pop();
+
+ ModbusRequest request = null;
+ if (functionCode == FunctionCode.READ_COILS)
+ request = new ReadCoilsRequest(slaveId);
+ else if (functionCode == FunctionCode.READ_DISCRETE_INPUTS)
+ request = new ReadDiscreteInputsRequest(slaveId);
+ else if (functionCode == FunctionCode.READ_HOLDING_REGISTERS)
+ request = new ReadHoldingRegistersRequest(slaveId);
+ else if (functionCode == FunctionCode.READ_INPUT_REGISTERS)
+ request = new ReadInputRegistersRequest(slaveId);
+ else if (functionCode == FunctionCode.WRITE_COIL)
+ request = new WriteCoilRequest(slaveId);
+ else if (functionCode == FunctionCode.WRITE_REGISTER)
+ request = new WriteRegisterRequest(slaveId);
+ else if (functionCode == FunctionCode.READ_EXCEPTION_STATUS)
+ request = new ReadExceptionStatusRequest(slaveId);
+ else if (functionCode == FunctionCode.WRITE_COILS)
+ request = new WriteCoilsRequest(slaveId);
+ else if (functionCode == FunctionCode.WRITE_REGISTERS)
+ request = new WriteRegistersRequest(slaveId);
+ else if (functionCode == FunctionCode.REPORT_SLAVE_ID)
+ request = new ReportSlaveIdRequest(slaveId);
+ // else if (functionCode == FunctionCode.WRITE_MASK_REGISTER)
+ // request = new WriteMaskRegisterRequest(slaveId);
+ else
+ request = new ExceptionRequest(slaveId, functionCode, ExceptionCode.ILLEGAL_FUNCTION);
+
+ request.readRequest(queue);
+
+ return request;
+ }
+
+ /**
+ * validate.
+ *
+ * @param modbus a {@link Modbus} object.
+ * @throws ModbusTransportException if any.
+ */
+ abstract public void validate(Modbus modbus) throws ModbusTransportException;
+
+ /**
+ * handle.
+ *
+ * @param processImage a {@link ProcessImage} object.
+ * @return a {@link ModbusResponse} object.
+ * @throws ModbusTransportException if any.
+ */
+ public ModbusResponse handle(ProcessImage processImage) throws ModbusTransportException {
+ try {
+ try {
+ return handleImpl(processImage);
+ } catch (IllegalDataAddressException e) {
+ return handleException(ExceptionCode.ILLEGAL_DATA_ADDRESS);
+ }
+ } catch (Exception e) {
+ return handleException(ExceptionCode.SLAVE_DEVICE_FAILURE);
+ }
+ }
+
+ abstract ModbusResponse handleImpl(ProcessImage processImage) throws ModbusTransportException;
+
+ /**
+ * readRequest.
+ *
+ * @param queue a {@link ByteQueue} object.
+ */
+ abstract protected void readRequest(ByteQueue queue);
+
+ ModbusResponse handleException(byte exceptionCode) throws ModbusTransportException {
+ ModbusResponse response = getResponseInstance(slaveId);
+ response.setException(exceptionCode);
+ return response;
+ }
+
+ abstract ModbusResponse getResponseInstance(int slaveId) throws ModbusTransportException;
+
+ @Override
+ final protected void writeImpl(ByteQueue queue) {
+ queue.push(getFunctionCode());
+ writeRequest(queue);
+ }
+
+ /**
+ * writeRequest.
+ *
+ * @param queue a {@link ByteQueue} object.
+ */
+ abstract protected void writeRequest(ByteQueue queue);
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ModbusResponse.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ModbusResponse.java
new file mode 100644
index 0000000..dd898c0
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ModbusResponse.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.ModbusUtils;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.ExceptionCode;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.FunctionCode;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.IllegalFunctionException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.SlaveIdNotEqual;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * Abstract ModbusResponse class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+abstract public class ModbusResponse extends ModbusMessage {
+ /**
+ * Constant MAX_FUNCTION_CODE=(byte) 0x80
+ */
+ protected static final byte MAX_FUNCTION_CODE = (byte) 0x80;
+ protected byte exceptionCode = -1;
+
+ ModbusResponse(int slaveId) throws ModbusTransportException {
+ super(slaveId);
+ }
+
+ /**
+ * createModbusResponse.
+ *
+ * @param queue a {@link ByteQueue} object.
+ * @return a {@link ModbusResponse} object.
+ * @throws ModbusTransportException if any.
+ */
+ public static ModbusResponse createModbusResponse(ByteQueue queue) throws ModbusTransportException {
+ int slaveId = ModbusUtils.popUnsignedByte(queue);
+ byte functionCode = queue.pop();
+ boolean isException = false;
+
+ if (greaterThan(functionCode, MAX_FUNCTION_CODE)) {
+ isException = true;
+ functionCode -= MAX_FUNCTION_CODE;
+ }
+
+ ModbusResponse response = null;
+ if (functionCode == FunctionCode.READ_COILS)
+ response = new ReadCoilsResponse(slaveId);
+ else if (functionCode == FunctionCode.READ_DISCRETE_INPUTS)
+ response = new ReadDiscreteInputsResponse(slaveId);
+ else if (functionCode == FunctionCode.READ_HOLDING_REGISTERS)
+ response = new ReadHoldingRegistersResponse(slaveId);
+ else if (functionCode == FunctionCode.READ_INPUT_REGISTERS)
+ response = new ReadInputRegistersResponse(slaveId);
+ else if (functionCode == FunctionCode.WRITE_COIL)
+ response = new WriteCoilResponse(slaveId);
+ else if (functionCode == FunctionCode.WRITE_REGISTER)
+ response = new WriteRegisterResponse(slaveId);
+ else if (functionCode == FunctionCode.READ_EXCEPTION_STATUS)
+ response = new ReadExceptionStatusResponse(slaveId);
+ else if (functionCode == FunctionCode.WRITE_COILS)
+ response = new WriteCoilsResponse(slaveId);
+ else if (functionCode == FunctionCode.WRITE_REGISTERS)
+ response = new WriteRegistersResponse(slaveId);
+ else if (functionCode == FunctionCode.REPORT_SLAVE_ID)
+ response = new ReportSlaveIdResponse(slaveId);
+ else if (functionCode == FunctionCode.WRITE_MASK_REGISTER)
+ response = new WriteMaskRegisterResponse(slaveId);
+ else
+ throw new IllegalFunctionException(functionCode, slaveId);
+
+ response.read(queue, isException);
+
+ return response;
+ }
+
+ private static boolean greaterThan(byte b1, byte b2) {
+ int i1 = b1 & 0xff;
+ int i2 = b2 & 0xff;
+ return i1 > i2;
+ }
+
+ /**
+ * main.
+ *
+ * @param args an array of {@link String} objects.
+ * @throws Exception if any.
+ */
+ public static void main(String[] args) throws Exception {
+ ByteQueue queue = new ByteQueue(new byte[]{3, 2});
+ ModbusResponse r = createModbusResponse(queue);
+ System.out.println(r);
+ }
+
+ /**
+ * isException.
+ *
+ * @return a boolean.
+ */
+ public boolean isException() {
+ return exceptionCode != -1;
+ }
+
+ void setException(byte exceptionCode) {
+ this.exceptionCode = exceptionCode;
+ }
+
+ /**
+ * getExceptionMessage.
+ *
+ * @return a {@link String} object.
+ */
+ public String getExceptionMessage() {
+ return ExceptionCode.getExceptionMessage(exceptionCode);
+ }
+
+ /**
+ * Getter for the field exceptionCode.
+ *
+ * @return a byte.
+ */
+ public byte getExceptionCode() {
+ return exceptionCode;
+ }
+
+ @Override
+ final protected void writeImpl(ByteQueue queue) {
+ if (isException()) {
+ queue.push((byte) (getFunctionCode() + MAX_FUNCTION_CODE));
+ queue.push(exceptionCode);
+ } else {
+ queue.push(getFunctionCode());
+ writeResponse(queue);
+ }
+ }
+
+ /**
+ * writeResponse.
+ *
+ * @param queue a {@link ByteQueue} object.
+ */
+ abstract protected void writeResponse(ByteQueue queue);
+
+ void read(ByteQueue queue, boolean isException) {
+ if (isException)
+ exceptionCode = queue.pop();
+ else
+ readResponse(queue);
+ }
+
+ /**
+ * readResponse.
+ *
+ * @param queue a {@link ByteQueue} object.
+ */
+ abstract protected void readResponse(ByteQueue queue);
+
+ /**
+ * Ensure that the Response slave id is equal to the requested slave id
+ *
+ * @param request
+ * @throws ModbusTransportException
+ */
+ public void validateResponse(ModbusRequest request) throws ModbusTransportException {
+ if (getSlaveId() != request.slaveId)
+ throw new SlaveIdNotEqual(request.slaveId, getSlaveId());
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadBinaryRequest.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadBinaryRequest.java
new file mode 100644
index 0000000..8520454
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadBinaryRequest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.Modbus;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ProcessImage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.ModbusUtils;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * Abstract ReadBinaryRequest class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+abstract public class ReadBinaryRequest extends ModbusRequest {
+ private int startOffset;
+ private int numberOfBits;
+
+ /**
+ * Constructor for ReadBinaryRequest.
+ *
+ * @param slaveId a int.
+ * @param startOffset a int.
+ * @param numberOfBits a int.
+ * @throws ModbusTransportException if any.
+ */
+ public ReadBinaryRequest(int slaveId, int startOffset, int numberOfBits) throws ModbusTransportException {
+ super(slaveId);
+ this.startOffset = startOffset;
+ this.numberOfBits = numberOfBits;
+ }
+
+ ReadBinaryRequest(int slaveId) throws ModbusTransportException {
+ super(slaveId);
+ }
+
+ @Override
+ public void validate(Modbus modbus) throws ModbusTransportException {
+ ModbusUtils.validateOffset(startOffset);
+ modbus.validateNumberOfBits(numberOfBits);
+ ModbusUtils.validateEndOffset(startOffset + numberOfBits - 1);
+ }
+
+ @Override
+ protected void writeRequest(ByteQueue queue) {
+ ModbusUtils.pushShort(queue, startOffset);
+ ModbusUtils.pushShort(queue, numberOfBits);
+ }
+
+ @Override
+ protected void readRequest(ByteQueue queue) {
+ startOffset = ModbusUtils.popUnsignedShort(queue);
+ numberOfBits = ModbusUtils.popUnsignedShort(queue);
+ }
+
+ /**
+ * getData.
+ *
+ * @param processImage a {@link ProcessImage} object.
+ * @return an array of {@link byte} objects.
+ * @throws ModbusTransportException if any.
+ */
+ protected byte[] getData(ProcessImage processImage) throws ModbusTransportException {
+ boolean[] data = new boolean[numberOfBits];
+
+ // Get the data from the process image.
+ for (int i = 0; i < numberOfBits; i++)
+ data[i] = getBinary(processImage, i + startOffset);
+
+ // Convert the boolean array into an array of bytes.
+ return convertToBytes(data);
+ }
+
+ /**
+ * getBinary.
+ *
+ * @param processImage a {@link ProcessImage} object.
+ * @param index a int.
+ * @return a boolean.
+ * @throws ModbusTransportException if any.
+ */
+ abstract protected boolean getBinary(ProcessImage processImage, int index) throws ModbusTransportException;
+
+ @Override
+ public String toString() {
+ return "ReadBinaryRequest [startOffset=" + startOffset + ", numberOfBits=" + numberOfBits + "]";
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadCoilsRequest.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadCoilsRequest.java
new file mode 100644
index 0000000..fa3dde2
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadCoilsRequest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ProcessImage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.FunctionCode;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+
+/**
+ * ReadCoilsRequest class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class ReadCoilsRequest extends ReadBinaryRequest {
+ /**
+ * Constructor for ReadCoilsRequest.
+ *
+ * @param slaveId a int.
+ * @param startOffset a int.
+ * @param numberOfBits a int.
+ * @throws ModbusTransportException if any.
+ */
+ public ReadCoilsRequest(int slaveId, int startOffset, int numberOfBits) throws ModbusTransportException {
+ super(slaveId, startOffset, numberOfBits);
+ }
+
+ ReadCoilsRequest(int slaveId) throws ModbusTransportException {
+ super(slaveId);
+ }
+
+ @Override
+ public byte getFunctionCode() {
+ return FunctionCode.READ_COILS;
+ }
+
+ @Override
+ ModbusResponse handleImpl(ProcessImage processImage) throws ModbusTransportException {
+ return new ReadCoilsResponse(slaveId, getData(processImage));
+ }
+
+ @Override
+ protected boolean getBinary(ProcessImage processImage, int index) throws ModbusTransportException {
+ return processImage.getCoil(index);
+ }
+
+ @Override
+ ModbusResponse getResponseInstance(int slaveId) throws ModbusTransportException {
+ return new ReadCoilsResponse(slaveId);
+ }
+
+ @Override
+ public String toString() {
+ return "ReadCoilsRequest [slaveId=" + slaveId + ", getFunctionCode()=" + getFunctionCode() + ", toString()="
+ + super.toString() + "]";
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadCoilsResponse.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadCoilsResponse.java
new file mode 100644
index 0000000..81f3aa3
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadCoilsResponse.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.FunctionCode;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+
+/**
+ * ReadCoilsResponse class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class ReadCoilsResponse extends ReadResponse {
+ ReadCoilsResponse(int slaveId, byte[] data) throws ModbusTransportException {
+ super(slaveId, data);
+ }
+
+ ReadCoilsResponse(int slaveId) throws ModbusTransportException {
+ super(slaveId);
+ }
+
+ @Override
+ public byte getFunctionCode() {
+ return FunctionCode.READ_COILS;
+ }
+
+ @Override
+ public String toString() {
+ return "ReadCoilsResponse [exceptionCode=" + exceptionCode + ", slaveId=" + slaveId + ", getFunctionCode()="
+ + getFunctionCode() + ", isException()=" + isException() + ", getExceptionMessage()="
+ + getExceptionMessage() + ", getExceptionCode()=" + getExceptionCode() + ", toString()="
+ + super.toString(false) + "]";
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadDiscreteInputsRequest.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadDiscreteInputsRequest.java
new file mode 100644
index 0000000..a0f07f2
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadDiscreteInputsRequest.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ProcessImage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.FunctionCode;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+
+/**
+ * ReadDiscreteInputsRequest class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class ReadDiscreteInputsRequest extends ReadBinaryRequest {
+ /**
+ * Constructor for ReadDiscreteInputsRequest.
+ *
+ * @param slaveId a int.
+ * @param startOffset a int.
+ * @param numberOfBits a int.
+ * @throws ModbusTransportException if any.
+ */
+ public ReadDiscreteInputsRequest(int slaveId, int startOffset, int numberOfBits) throws ModbusTransportException {
+ super(slaveId, startOffset, numberOfBits);
+ }
+
+ ReadDiscreteInputsRequest(int slaveId) throws ModbusTransportException {
+ super(slaveId);
+ }
+
+ @Override
+ public byte getFunctionCode() {
+ return FunctionCode.READ_DISCRETE_INPUTS;
+ }
+
+ @Override
+ ModbusResponse handleImpl(ProcessImage processImage) throws ModbusTransportException {
+ return new ReadDiscreteInputsResponse(slaveId, getData(processImage));
+ }
+
+ @Override
+ protected boolean getBinary(ProcessImage processImage, int index) throws ModbusTransportException {
+ return processImage.getInput(index);
+ }
+
+ @Override
+ ModbusResponse getResponseInstance(int slaveId) throws ModbusTransportException {
+ return new ReadDiscreteInputsResponse(slaveId);
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadDiscreteInputsResponse.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadDiscreteInputsResponse.java
new file mode 100644
index 0000000..db67c06
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadDiscreteInputsResponse.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.FunctionCode;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+
+/**
+ * ReadDiscreteInputsResponse class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class ReadDiscreteInputsResponse extends ReadResponse {
+ ReadDiscreteInputsResponse(int slaveId, byte[] data) throws ModbusTransportException {
+ super(slaveId, data);
+ }
+
+ ReadDiscreteInputsResponse(int slaveId) throws ModbusTransportException {
+ super(slaveId);
+ }
+
+ @Override
+ public byte getFunctionCode() {
+ return FunctionCode.READ_DISCRETE_INPUTS;
+ }
+
+ @Override
+ public String toString() {
+ return "ReadDiscreteInputsResponse [exceptionCode=" + exceptionCode + ", slaveId=" + slaveId
+ + ", getFunctionCode()=" + getFunctionCode() + ", isException()=" + isException()
+ + ", getExceptionMessage()=" + getExceptionMessage() + ", getExceptionCode()=" + getExceptionCode()
+ + super.toString(false) + "]";
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadExceptionStatusRequest.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadExceptionStatusRequest.java
new file mode 100644
index 0000000..661a9e3
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadExceptionStatusRequest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.Modbus;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ProcessImage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.FunctionCode;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * ReadExceptionStatusRequest class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class ReadExceptionStatusRequest extends ModbusRequest {
+ /**
+ * Constructor for ReadExceptionStatusRequest.
+ *
+ * @param slaveId a int.
+ * @throws ModbusTransportException if any.
+ */
+ public ReadExceptionStatusRequest(int slaveId) throws ModbusTransportException {
+ super(slaveId);
+ }
+
+ @Override
+ public void validate(Modbus modbus) {
+ // no op
+ }
+
+ @Override
+ protected void writeRequest(ByteQueue queue) {
+ // no op
+ }
+
+ @Override
+ protected void readRequest(ByteQueue queue) {
+ // no op
+ }
+
+ @Override
+ ModbusResponse getResponseInstance(int slaveId) throws ModbusTransportException {
+ return new ReadExceptionStatusResponse(slaveId);
+ }
+
+ @Override
+ ModbusResponse handleImpl(ProcessImage processImage) throws ModbusTransportException {
+ return new ReadExceptionStatusResponse(slaveId, processImage.getExceptionStatus());
+ }
+
+ @Override
+ public byte getFunctionCode() {
+ return FunctionCode.READ_EXCEPTION_STATUS;
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadExceptionStatusResponse.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadExceptionStatusResponse.java
new file mode 100644
index 0000000..38113a9
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadExceptionStatusResponse.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.FunctionCode;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * ReadExceptionStatusResponse class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class ReadExceptionStatusResponse extends ModbusResponse {
+ private byte exceptionStatus;
+
+ ReadExceptionStatusResponse(int slaveId) throws ModbusTransportException {
+ super(slaveId);
+ }
+
+ ReadExceptionStatusResponse(int slaveId, byte exceptionStatus) throws ModbusTransportException {
+ super(slaveId);
+ this.exceptionStatus = exceptionStatus;
+ }
+
+
+ @Override
+ public byte getFunctionCode() {
+ return FunctionCode.READ_EXCEPTION_STATUS;
+ }
+
+
+ @Override
+ protected void readResponse(ByteQueue queue) {
+ exceptionStatus = queue.pop();
+ }
+
+
+ @Override
+ protected void writeResponse(ByteQueue queue) {
+ queue.push(exceptionStatus);
+ }
+
+ /**
+ * Getter for the field exceptionStatus.
+ *
+ * @return a byte.
+ */
+ public byte getExceptionStatus() {
+ return exceptionStatus;
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadHoldingRegistersRequest.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadHoldingRegistersRequest.java
new file mode 100644
index 0000000..7e2fd40
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadHoldingRegistersRequest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ProcessImage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.FunctionCode;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+
+/**
+ * ReadHoldingRegistersRequest class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class ReadHoldingRegistersRequest extends ReadNumericRequest {
+ /**
+ * Constructor for ReadHoldingRegistersRequest.
+ *
+ * @param slaveId a int.
+ * @param startOffset a int.
+ * @param numberOfRegisters a int.
+ * @throws ModbusTransportException if any.
+ */
+ public ReadHoldingRegistersRequest(int slaveId, int startOffset, int numberOfRegisters)
+ throws ModbusTransportException {
+ super(slaveId, startOffset, numberOfRegisters);
+ }
+
+ ReadHoldingRegistersRequest(int slaveId) throws ModbusTransportException {
+ super(slaveId);
+ }
+
+
+ @Override
+ public byte getFunctionCode() {
+ return FunctionCode.READ_HOLDING_REGISTERS;
+ }
+
+ @Override
+ ModbusResponse handleImpl(ProcessImage processImage) throws ModbusTransportException {
+ return new ReadHoldingRegistersResponse(slaveId, getData(processImage));
+ }
+
+
+ @Override
+ protected short getNumeric(ProcessImage processImage, int index) throws ModbusTransportException {
+ return processImage.getHoldingRegister(index);
+ }
+
+ @Override
+ ModbusResponse getResponseInstance(int slaveId) throws ModbusTransportException {
+ return new ReadHoldingRegistersResponse(slaveId);
+ }
+
+
+ @Override
+ public String toString() {
+ return "ReadHoldingRegistersRequest [slaveId=" + slaveId + ", getFunctionCode()=" + getFunctionCode()
+ + ", toString()=" + super.toString() + "]";
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadHoldingRegistersResponse.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadHoldingRegistersResponse.java
new file mode 100644
index 0000000..a23fbf5
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadHoldingRegistersResponse.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.FunctionCode;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+
+/**
+ * ReadHoldingRegistersResponse class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class ReadHoldingRegistersResponse extends ReadResponse {
+ ReadHoldingRegistersResponse(int slaveId, byte[] data) throws ModbusTransportException {
+ super(slaveId, data);
+ }
+
+ ReadHoldingRegistersResponse(int slaveId) throws ModbusTransportException {
+ super(slaveId);
+ }
+
+ @Override
+ public byte getFunctionCode() {
+ return FunctionCode.READ_HOLDING_REGISTERS;
+ }
+
+ @Override
+ public String toString() {
+ return "ReadHoldingRegistersResponse [exceptionCode=" + exceptionCode + ", slaveId=" + slaveId
+ + ", getFunctionCode()=" + getFunctionCode() + ", isException()=" + isException()
+ + ", getExceptionMessage()=" + getExceptionMessage() + ", getExceptionCode()=" + getExceptionCode()
+ + ", toString()=" + super.toString(true) + "]";
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadInputRegistersRequest.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadInputRegistersRequest.java
new file mode 100644
index 0000000..0b70b3e
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadInputRegistersRequest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ProcessImage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.FunctionCode;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+
+/**
+ * ReadInputRegistersRequest class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class ReadInputRegistersRequest extends ReadNumericRequest {
+ /**
+ * Constructor for ReadInputRegistersRequest.
+ *
+ * @param slaveId a int.
+ * @param startOffset a int.
+ * @param numberOfRegisters a int.
+ * @throws ModbusTransportException if any.
+ */
+ public ReadInputRegistersRequest(int slaveId, int startOffset, int numberOfRegisters)
+ throws ModbusTransportException {
+ super(slaveId, startOffset, numberOfRegisters);
+ }
+
+ ReadInputRegistersRequest(int slaveId) throws ModbusTransportException {
+ super(slaveId);
+ }
+
+ @Override
+ public byte getFunctionCode() {
+ return FunctionCode.READ_INPUT_REGISTERS;
+ }
+
+ @Override
+ ModbusResponse handleImpl(ProcessImage processImage) throws ModbusTransportException {
+ return new ReadInputRegistersResponse(slaveId, getData(processImage));
+ }
+
+ @Override
+ protected short getNumeric(ProcessImage processImage, int index) throws ModbusTransportException {
+ return processImage.getInputRegister(index);
+ }
+
+ @Override
+ ModbusResponse getResponseInstance(int slaveId) throws ModbusTransportException {
+ return new ReadInputRegistersResponse(slaveId);
+ }
+
+ @Override
+ public String toString() {
+ return "ReadInputRegistersRequest [slaveId=" + slaveId + ", getFunctionCode()=" + getFunctionCode()
+ + ", toString()=" + super.toString() + "]";
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadInputRegistersResponse.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadInputRegistersResponse.java
new file mode 100644
index 0000000..f5043d4
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadInputRegistersResponse.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.FunctionCode;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+
+/**
+ * ReadInputRegistersResponse class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class ReadInputRegistersResponse extends ReadResponse {
+ ReadInputRegistersResponse(int slaveId, byte[] data) throws ModbusTransportException {
+ super(slaveId, data);
+ }
+
+ ReadInputRegistersResponse(int slaveId) throws ModbusTransportException {
+ super(slaveId);
+ }
+
+ @Override
+ public byte getFunctionCode() {
+ return FunctionCode.READ_INPUT_REGISTERS;
+ }
+
+ @Override
+ public String toString() {
+ return "ReadInputRegistersResponse [exceptionCode=" + exceptionCode + ", slaveId=" + slaveId
+ + ", getFunctionCode()=" + getFunctionCode() + ", isException()=" + isException()
+ + ", getExceptionMessage()=" + getExceptionMessage() + ", getExceptionCode()=" + getExceptionCode()
+ + ", toString()=" + super.toString(true) + "]";
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadNumericRequest.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadNumericRequest.java
new file mode 100644
index 0000000..fc96b8d
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadNumericRequest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.Modbus;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ProcessImage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.ModbusUtils;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * Abstract ReadNumericRequest class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+abstract public class ReadNumericRequest extends ModbusRequest {
+ private int startOffset;
+ private int numberOfRegisters;
+
+ /**
+ * Constructor for ReadNumericRequest.
+ *
+ * @param slaveId a int.
+ * @param startOffset a int.
+ * @param numberOfRegisters a int.
+ * @throws ModbusTransportException if any.
+ */
+ public ReadNumericRequest(int slaveId, int startOffset, int numberOfRegisters) throws ModbusTransportException {
+ super(slaveId);
+ this.startOffset = startOffset;
+ this.numberOfRegisters = numberOfRegisters;
+ }
+
+ ReadNumericRequest(int slaveId) throws ModbusTransportException {
+ super(slaveId);
+ }
+
+ @Override
+ public void validate(Modbus modbus) throws ModbusTransportException {
+ ModbusUtils.validateOffset(startOffset);
+ modbus.validateNumberOfRegisters(numberOfRegisters);
+ ModbusUtils.validateEndOffset(startOffset + numberOfRegisters - 1);
+ }
+
+ @Override
+ protected void writeRequest(ByteQueue queue) {
+ ModbusUtils.pushShort(queue, startOffset);
+ ModbusUtils.pushShort(queue, numberOfRegisters);
+ }
+
+ @Override
+ protected void readRequest(ByteQueue queue) {
+ startOffset = ModbusUtils.popUnsignedShort(queue);
+ numberOfRegisters = ModbusUtils.popUnsignedShort(queue);
+ }
+
+ /**
+ * getData.
+ *
+ * @param processImage a {@link ProcessImage} object.
+ * @return an array of {@link byte} objects.
+ * @throws ModbusTransportException if any.
+ */
+ protected byte[] getData(ProcessImage processImage) throws ModbusTransportException {
+ short[] data = new short[numberOfRegisters];
+
+ // Get the data from the process image.
+ for (int i = 0; i < numberOfRegisters; i++)
+ data[i] = getNumeric(processImage, i + startOffset);
+
+ return convertToBytes(data);
+ }
+
+ /**
+ * getNumeric.
+ *
+ * @param processImage a {@link ProcessImage} object.
+ * @param index a int.
+ * @return a short.
+ * @throws ModbusTransportException if any.
+ */
+ abstract protected short getNumeric(ProcessImage processImage, int index) throws ModbusTransportException;
+
+ @Override
+ public String toString() {
+ return "ReadNumericRequest [startOffset=" + startOffset + ", numberOfRegisters=" + numberOfRegisters + "]";
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadResponse.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadResponse.java
new file mode 100644
index 0000000..cc30dbd
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReadResponse.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.ModbusUtils;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.io.StreamUtils;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * Abstract ReadResponse class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+abstract public class ReadResponse extends ModbusResponse {
+ private byte[] data;
+
+ ReadResponse(int slaveId) throws ModbusTransportException {
+ super(slaveId);
+ }
+
+ ReadResponse(int slaveId, byte[] data) throws ModbusTransportException {
+ super(slaveId);
+ this.data = data;
+ }
+
+ @Override
+ protected void readResponse(ByteQueue queue) {
+ int numberOfBytes = ModbusUtils.popUnsignedByte(queue);
+ if (queue.size() < numberOfBytes)
+ throw new ArrayIndexOutOfBoundsException();
+
+ data = new byte[numberOfBytes];
+ queue.pop(data);
+ }
+
+ @Override
+ protected void writeResponse(ByteQueue queue) {
+ ModbusUtils.pushByte(queue, data.length);
+ queue.push(data);
+ }
+
+ /**
+ * Getter for the field data.
+ *
+ * @return an array of {@link byte} objects.
+ */
+ public byte[] getData() {
+ return data;
+ }
+
+ /**
+ * getShortData.
+ *
+ * @return an array of {@link short} objects.
+ */
+ public short[] getShortData() {
+ return convertToShorts(data);
+ }
+
+ /**
+ * getBooleanData.
+ *
+ * @return an array of {@link boolean} objects.
+ */
+ public boolean[] getBooleanData() {
+ return convertToBooleans(data);
+ }
+
+ /**
+ * toString.
+ *
+ * @param numeric a boolean.
+ * @return a {@link String} object.
+ */
+ public String toString(boolean numeric) {
+ if (data == null)
+ return "ReadResponse [null]";
+ return "ReadResponse [len=" + (numeric ? data.length / 2 : data.length * 8) + ", " + StreamUtils.dumpHex(data)
+ + "]";
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReportSlaveIdRequest.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReportSlaveIdRequest.java
new file mode 100644
index 0000000..9b37976
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReportSlaveIdRequest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.Modbus;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ProcessImage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.FunctionCode;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * ReportSlaveIdRequest class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class ReportSlaveIdRequest extends ModbusRequest {
+ /**
+ * Constructor for ReportSlaveIdRequest.
+ *
+ * @param slaveId a int.
+ * @throws ModbusTransportException if any.
+ */
+ public ReportSlaveIdRequest(int slaveId) throws ModbusTransportException {
+ super(slaveId);
+ }
+
+ @Override
+ public void validate(Modbus modbus) {
+ // no op
+ }
+
+ @Override
+ protected void writeRequest(ByteQueue queue) {
+ // no op
+ }
+
+ @Override
+ protected void readRequest(ByteQueue queue) {
+ // no op
+ }
+
+ @Override
+ ModbusResponse getResponseInstance(int slaveId) throws ModbusTransportException {
+ return new ReportSlaveIdResponse(slaveId);
+ }
+
+ @Override
+ ModbusResponse handleImpl(ProcessImage processImage) throws ModbusTransportException {
+ return new ReportSlaveIdResponse(slaveId, processImage.getReportSlaveIdData());
+ }
+
+ @Override
+ public byte getFunctionCode() {
+ return FunctionCode.REPORT_SLAVE_ID;
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReportSlaveIdResponse.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReportSlaveIdResponse.java
new file mode 100644
index 0000000..671b488
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/ReportSlaveIdResponse.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.ModbusUtils;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.FunctionCode;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * ReportSlaveIdResponse class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class ReportSlaveIdResponse extends ModbusResponse {
+ private byte[] data;
+
+ ReportSlaveIdResponse(int slaveId) throws ModbusTransportException {
+ super(slaveId);
+ }
+
+ ReportSlaveIdResponse(int slaveId, byte[] data) throws ModbusTransportException {
+ super(slaveId);
+ this.data = data;
+ }
+
+ @Override
+ public byte getFunctionCode() {
+ return FunctionCode.REPORT_SLAVE_ID;
+ }
+
+ @Override
+ protected void readResponse(ByteQueue queue) {
+ int numberOfBytes = ModbusUtils.popUnsignedByte(queue);
+ if (queue.size() < numberOfBytes)
+ throw new ArrayIndexOutOfBoundsException();
+
+ data = new byte[numberOfBytes];
+ queue.pop(data);
+ }
+
+ @Override
+ protected void writeResponse(ByteQueue queue) {
+ ModbusUtils.pushByte(queue, data.length);
+ queue.push(data);
+ }
+
+ /**
+ * Getter for the field data.
+ *
+ * @return an array of {@link byte} objects.
+ */
+ public byte[] getData() {
+ return data;
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/WriteCoilRequest.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/WriteCoilRequest.java
new file mode 100644
index 0000000..5165f93
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/WriteCoilRequest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.Modbus;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ProcessImage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.ModbusUtils;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.FunctionCode;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * WriteCoilRequest class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class WriteCoilRequest extends ModbusRequest {
+ private int writeOffset;
+ private boolean writeValue;
+
+ /**
+ * Constructor for WriteCoilRequest.
+ *
+ * @param slaveId a int.
+ * @param writeOffset a int.
+ * @param writeValue a boolean.
+ * @throws ModbusTransportException if any.
+ */
+ public WriteCoilRequest(int slaveId, int writeOffset, boolean writeValue) throws ModbusTransportException {
+ super(slaveId);
+ this.writeOffset = writeOffset;
+ this.writeValue = writeValue;
+ }
+
+
+ WriteCoilRequest(int slaveId) throws ModbusTransportException {
+ super(slaveId);
+ }
+
+ @Override
+ public void validate(Modbus modbus) throws ModbusTransportException {
+ ModbusUtils.validateOffset(writeOffset);
+ }
+
+ @Override
+ protected void writeRequest(ByteQueue queue) {
+ ModbusUtils.pushShort(queue, writeOffset);
+ ModbusUtils.pushShort(queue, writeValue ? 0xff00 : 0);
+ }
+
+ @Override
+ ModbusResponse handleImpl(ProcessImage processImage) throws ModbusTransportException {
+ processImage.writeCoil(writeOffset, writeValue);
+ return new WriteCoilResponse(slaveId, writeOffset, writeValue);
+ }
+
+
+ @Override
+ public byte getFunctionCode() {
+ return FunctionCode.WRITE_COIL;
+ }
+
+ @Override
+ ModbusResponse getResponseInstance(int slaveId) throws ModbusTransportException {
+ return new WriteCoilResponse(slaveId);
+ }
+
+
+ @Override
+ protected void readRequest(ByteQueue queue) {
+ writeOffset = ModbusUtils.popUnsignedShort(queue);
+ writeValue = ModbusUtils.popUnsignedShort(queue) == 0xff00;
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/WriteCoilResponse.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/WriteCoilResponse.java
new file mode 100644
index 0000000..0bdcc7e
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/WriteCoilResponse.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.ModbusUtils;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.FunctionCode;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * WriteCoilResponse class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class WriteCoilResponse extends ModbusResponse {
+ private int writeOffset;
+ private boolean writeValue;
+
+
+ WriteCoilResponse(int slaveId) throws ModbusTransportException {
+ super(slaveId);
+ }
+
+ WriteCoilResponse(int slaveId, int writeOffset, boolean writeValue) throws ModbusTransportException {
+ super(slaveId);
+ this.writeOffset = writeOffset;
+ this.writeValue = writeValue;
+ }
+
+ @Override
+ public byte getFunctionCode() {
+ return FunctionCode.WRITE_COIL;
+ }
+
+ @Override
+ protected void writeResponse(ByteQueue queue) {
+ ModbusUtils.pushShort(queue, writeOffset);
+ ModbusUtils.pushShort(queue, writeValue ? 0xff00 : 0);
+ }
+
+
+ @Override
+ protected void readResponse(ByteQueue queue) {
+ writeOffset = ModbusUtils.popUnsignedShort(queue);
+ writeValue = ModbusUtils.popUnsignedShort(queue) == 0xff00;
+ }
+
+ /**
+ * Getter for the field writeOffset.
+ *
+ * @return a int.
+ */
+ public int getWriteOffset() {
+ return writeOffset;
+ }
+
+ /**
+ * isWriteValue.
+ *
+ * @return a boolean.
+ */
+ public boolean isWriteValue() {
+ return writeValue;
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/WriteCoilsRequest.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/WriteCoilsRequest.java
new file mode 100644
index 0000000..eaca685
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/WriteCoilsRequest.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.Modbus;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ProcessImage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.ModbusUtils;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.FunctionCode;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * WriteCoilsRequest class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class WriteCoilsRequest extends ModbusRequest {
+ private int startOffset;
+ private int numberOfBits;
+ private byte[] data;
+
+ /**
+ * Constructor for WriteCoilsRequest.
+ *
+ * @param slaveId a int.
+ * @param startOffset a int.
+ * @param bdata an array of {@link boolean} objects.
+ * @throws ModbusTransportException if any.
+ */
+ public WriteCoilsRequest(int slaveId, int startOffset, boolean[] bdata) throws ModbusTransportException {
+ super(slaveId);
+ this.startOffset = startOffset;
+ numberOfBits = bdata.length;
+ data = convertToBytes(bdata);
+ }
+
+ WriteCoilsRequest(int slaveId) throws ModbusTransportException {
+ super(slaveId);
+ }
+
+ @Override
+ public void validate(Modbus modbus) throws ModbusTransportException {
+ ModbusUtils.validateOffset(startOffset);
+ modbus.validateNumberOfBits(numberOfBits);
+ ModbusUtils.validateEndOffset(startOffset + numberOfBits - 1);
+ }
+
+ @Override
+ protected void writeRequest(ByteQueue queue) {
+ ModbusUtils.pushShort(queue, startOffset);
+ ModbusUtils.pushShort(queue, numberOfBits);
+ ModbusUtils.pushByte(queue, data.length);
+ queue.push(data);
+ }
+
+ @Override
+ ModbusResponse handleImpl(ProcessImage processImage) throws ModbusTransportException {
+ boolean[] bdata = convertToBooleans(data);
+ for (int i = 0; i < numberOfBits; i++)
+ processImage.writeCoil(startOffset + i, bdata[i]);
+ return new WriteCoilsResponse(slaveId, startOffset, numberOfBits);
+ }
+
+ @Override
+ public byte getFunctionCode() {
+ return FunctionCode.WRITE_COILS;
+ }
+
+ @Override
+ ModbusResponse getResponseInstance(int slaveId) throws ModbusTransportException {
+ return new WriteCoilsResponse(slaveId);
+ }
+
+ @Override
+ protected void readRequest(ByteQueue queue) {
+ startOffset = ModbusUtils.popUnsignedShort(queue);
+ numberOfBits = ModbusUtils.popUnsignedShort(queue);
+ data = new byte[ModbusUtils.popUnsignedByte(queue)];
+ queue.pop(data);
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/WriteCoilsResponse.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/WriteCoilsResponse.java
new file mode 100644
index 0000000..3894c5d
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/WriteCoilsResponse.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.ModbusUtils;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.FunctionCode;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * WriteCoilsResponse class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class WriteCoilsResponse extends ModbusResponse {
+ private int startOffset;
+ private int numberOfBits;
+
+ WriteCoilsResponse(int slaveId) throws ModbusTransportException {
+ super(slaveId);
+ }
+
+ WriteCoilsResponse(int slaveId, int startOffset, int numberOfBits) throws ModbusTransportException {
+ super(slaveId);
+ this.startOffset = startOffset;
+ this.numberOfBits = numberOfBits;
+ }
+
+ @Override
+ public byte getFunctionCode() {
+ return FunctionCode.WRITE_COILS;
+ }
+
+ @Override
+ protected void writeResponse(ByteQueue queue) {
+ ModbusUtils.pushShort(queue, startOffset);
+ ModbusUtils.pushShort(queue, numberOfBits);
+ }
+
+ @Override
+ protected void readResponse(ByteQueue queue) {
+ startOffset = ModbusUtils.popUnsignedShort(queue);
+ numberOfBits = ModbusUtils.popUnsignedShort(queue);
+ }
+
+ /**
+ * Getter for the field startOffset.
+ *
+ * @return a int.
+ */
+ public int getStartOffset() {
+ return startOffset;
+ }
+
+ /**
+ * Getter for the field numberOfBits.
+ *
+ * @return a int.
+ */
+ public int getNumberOfBits() {
+ return numberOfBits;
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/WriteMaskRegisterRequest.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/WriteMaskRegisterRequest.java
new file mode 100644
index 0000000..217c7f9
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/WriteMaskRegisterRequest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.Modbus;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ProcessImage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.ModbusUtils;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.FunctionCode;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusIdException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * WriteMaskRegisterRequest class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class WriteMaskRegisterRequest extends ModbusRequest {
+ private int writeOffset;
+
+ /**
+ * The andMask determines which bits we want to change. If a bit in the andMask is 1, it indicates that the value
+ * should not be changed. If it is zero, it should be changed according to the orMask value for that bit.
+ */
+ private int andMask;
+
+ /**
+ * The orMask determines what value a bit will have after writing if the andMask allows that bit to be changed. If a
+ * changable bit in the orMask is 0, the bit in the result will be zero. Ditto for 1.
+ */
+ private int orMask;
+
+ /**
+ * Constructor that defaults the masks to have no effect on the register. Use the setBit function to modify mask
+ * values.
+ *
+ * @param slaveId a int.
+ * @param writeOffset a int.
+ * @throws ModbusTransportException when necessary
+ */
+ public WriteMaskRegisterRequest(int slaveId, int writeOffset) throws ModbusTransportException {
+ this(slaveId, writeOffset, 0xffff, 0);
+ }
+
+ /**
+ * Constructor for WriteMaskRegisterRequest.
+ *
+ * @param slaveId a int.
+ * @param writeOffset a int.
+ * @param andMask a int.
+ * @param orMask a int.
+ * @throws ModbusTransportException if any.
+ */
+ public WriteMaskRegisterRequest(int slaveId, int writeOffset, int andMask, int orMask)
+ throws ModbusTransportException {
+ super(slaveId);
+ this.writeOffset = writeOffset;
+ this.andMask = andMask;
+ this.orMask = orMask;
+ }
+
+ WriteMaskRegisterRequest(int slaveId) throws ModbusTransportException {
+ super(slaveId);
+ }
+
+ @Override
+ public void validate(Modbus modbus) throws ModbusTransportException {
+ ModbusUtils.validateOffset(writeOffset);
+ }
+
+ /**
+ * setBit.
+ *
+ * @param bit a int.
+ * @param value a boolean.
+ */
+ public void setBit(int bit, boolean value) {
+ if (bit < 0 || bit > 15)
+ throw new ModbusIdException("Bit must be between 0 and 15 inclusive");
+
+ // Set the bit in the andMask to 0 to allow writing.
+ andMask = andMask & ~(1 << bit);
+
+ // Set the bit in the orMask to write the correct value.
+ if (value)
+ orMask = orMask | 1 << bit;
+ else
+ orMask = orMask & ~(1 << bit);
+ }
+
+ @Override
+ protected void writeRequest(ByteQueue queue) {
+ ModbusUtils.pushShort(queue, writeOffset);
+ ModbusUtils.pushShort(queue, andMask);
+ ModbusUtils.pushShort(queue, orMask);
+ }
+
+ @Override
+ ModbusResponse handleImpl(ProcessImage processImage) throws ModbusTransportException {
+ short value = processImage.getHoldingRegister(writeOffset);
+ value = (short) ((value & andMask) | (orMask & (~andMask)));
+ processImage.writeHoldingRegister(writeOffset, value);
+ return new WriteMaskRegisterResponse(slaveId, writeOffset, andMask, orMask);
+ }
+
+ @Override
+ public byte getFunctionCode() {
+ return FunctionCode.WRITE_MASK_REGISTER;
+ }
+
+ @Override
+ ModbusResponse getResponseInstance(int slaveId) throws ModbusTransportException {
+ return new WriteMaskRegisterResponse(slaveId);
+ }
+
+ @Override
+ protected void readRequest(ByteQueue queue) {
+ writeOffset = ModbusUtils.popUnsignedShort(queue);
+ andMask = ModbusUtils.popUnsignedShort(queue);
+ orMask = ModbusUtils.popUnsignedShort(queue);
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/WriteMaskRegisterResponse.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/WriteMaskRegisterResponse.java
new file mode 100644
index 0000000..7d169dd
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/WriteMaskRegisterResponse.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.ModbusUtils;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.FunctionCode;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * WriteMaskRegisterResponse class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class WriteMaskRegisterResponse extends ModbusResponse {
+ private int writeOffset;
+ private int andMask;
+ private int orMask;
+
+ WriteMaskRegisterResponse(int slaveId) throws ModbusTransportException {
+ super(slaveId);
+ }
+
+ WriteMaskRegisterResponse(int slaveId, int writeOffset, int andMask, int orMask) throws ModbusTransportException {
+ super(slaveId);
+ this.writeOffset = writeOffset;
+ this.andMask = andMask;
+ this.orMask = orMask;
+ }
+
+ @Override
+ public byte getFunctionCode() {
+ return FunctionCode.WRITE_MASK_REGISTER;
+ }
+
+ @Override
+ protected void writeResponse(ByteQueue queue) {
+ ModbusUtils.pushShort(queue, writeOffset);
+ ModbusUtils.pushShort(queue, andMask);
+ ModbusUtils.pushShort(queue, orMask);
+ }
+
+ @Override
+ protected void readResponse(ByteQueue queue) {
+ writeOffset = ModbusUtils.popUnsignedShort(queue);
+ andMask = ModbusUtils.popUnsignedShort(queue);
+ orMask = ModbusUtils.popUnsignedShort(queue);
+ }
+
+ /**
+ * Getter for the field writeOffset.
+ *
+ * @return a int.
+ */
+ public int getWriteOffset() {
+ return writeOffset;
+ }
+
+ /**
+ * Getter for the field andMask.
+ *
+ * @return a int.
+ */
+ public int getAndMask() {
+ return andMask;
+ }
+
+ /**
+ * Getter for the field orMask.
+ *
+ * @return a int.
+ */
+ public int getOrMask() {
+ return orMask;
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/WriteRegisterRequest.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/WriteRegisterRequest.java
new file mode 100644
index 0000000..6bf91ab
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/WriteRegisterRequest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.Modbus;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ProcessImage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.ModbusUtils;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.FunctionCode;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * WriteRegisterRequest class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class WriteRegisterRequest extends ModbusRequest {
+ private int writeOffset;
+ private int writeValue;
+
+ /**
+ * Constructor for WriteRegisterRequest.
+ *
+ * @param slaveId a int.
+ * @param writeOffset a int.
+ * @param writeValue a int.
+ * @throws ModbusTransportException if any.
+ */
+ public WriteRegisterRequest(int slaveId, int writeOffset, int writeValue) throws ModbusTransportException {
+ super(slaveId);
+ this.writeOffset = writeOffset;
+ this.writeValue = writeValue;
+ }
+
+ WriteRegisterRequest(int slaveId) throws ModbusTransportException {
+ super(slaveId);
+ }
+
+ @Override
+ public void validate(Modbus modbus) throws ModbusTransportException {
+ ModbusUtils.validateOffset(writeOffset);
+ }
+
+ @Override
+ protected void writeRequest(ByteQueue queue) {
+ ModbusUtils.pushShort(queue, writeOffset);
+ ModbusUtils.pushShort(queue, writeValue);
+ }
+
+ @Override
+ ModbusResponse handleImpl(ProcessImage processImage) throws ModbusTransportException {
+ processImage.writeHoldingRegister(writeOffset, (short) writeValue);
+ return new WriteRegisterResponse(slaveId, writeOffset, writeValue);
+ }
+
+ @Override
+ public byte getFunctionCode() {
+ return FunctionCode.WRITE_REGISTER;
+ }
+
+ @Override
+ ModbusResponse getResponseInstance(int slaveId) throws ModbusTransportException {
+ return new WriteRegisterResponse(slaveId);
+ }
+
+ @Override
+ protected void readRequest(ByteQueue queue) {
+ writeOffset = ModbusUtils.popUnsignedShort(queue);
+ writeValue = ModbusUtils.popUnsignedShort(queue);
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/WriteRegisterResponse.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/WriteRegisterResponse.java
new file mode 100644
index 0000000..ef3ee26
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/WriteRegisterResponse.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.ModbusUtils;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.FunctionCode;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * WriteRegisterResponse class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class WriteRegisterResponse extends ModbusResponse {
+ private int writeOffset;
+ private int writeValue;
+
+ WriteRegisterResponse(int slaveId) throws ModbusTransportException {
+ super(slaveId);
+ }
+
+ WriteRegisterResponse(int slaveId, int writeOffset, int writeValue) throws ModbusTransportException {
+ super(slaveId);
+ this.writeOffset = writeOffset;
+ this.writeValue = writeValue;
+ }
+
+ @Override
+ public byte getFunctionCode() {
+ return FunctionCode.WRITE_REGISTER;
+ }
+
+ @Override
+ protected void writeResponse(ByteQueue queue) {
+ ModbusUtils.pushShort(queue, writeOffset);
+ ModbusUtils.pushShort(queue, writeValue);
+ }
+
+ @Override
+ protected void readResponse(ByteQueue queue) {
+ writeOffset = ModbusUtils.popUnsignedShort(queue);
+ writeValue = ModbusUtils.popUnsignedShort(queue);
+ }
+
+ /**
+ * Getter for the field writeOffset.
+ *
+ * @return a int.
+ */
+ public int getWriteOffset() {
+ return writeOffset;
+ }
+
+ /**
+ * Getter for the field writeValue.
+ *
+ * @return a int.
+ */
+ public int getWriteValue() {
+ return writeValue;
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/WriteRegistersRequest.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/WriteRegistersRequest.java
new file mode 100644
index 0000000..349dfb6
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/WriteRegistersRequest.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.Modbus;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ProcessImage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.ModbusUtils;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.FunctionCode;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * WriteRegistersRequest class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class WriteRegistersRequest extends ModbusRequest {
+ private int startOffset;
+ private byte[] data;
+
+ /**
+ * Constructor for WriteRegistersRequest.
+ *
+ * @param slaveId a int.
+ * @param startOffset a int.
+ * @param sdata an array of {@link short} objects.
+ * @throws ModbusTransportException if any.
+ */
+ public WriteRegistersRequest(int slaveId, int startOffset, short[] sdata) throws ModbusTransportException {
+ super(slaveId);
+ this.startOffset = startOffset;
+ data = convertToBytes(sdata);
+ }
+
+ WriteRegistersRequest(int slaveId) throws ModbusTransportException {
+ super(slaveId);
+ }
+
+ @Override
+ public void validate(Modbus modbus) throws ModbusTransportException {
+ ModbusUtils.validateOffset(startOffset);
+ int registerCount = data.length / 2;
+ if (registerCount < 1 || registerCount > modbus.getMaxWriteRegisterCount())
+ throw new ModbusTransportException("Invalid number of registers: " + registerCount, slaveId);
+ ModbusUtils.validateEndOffset(startOffset + registerCount - 1);
+ }
+
+ @Override
+ protected void writeRequest(ByteQueue queue) {
+ ModbusUtils.pushShort(queue, startOffset);
+ ModbusUtils.pushShort(queue, data.length / 2);
+ ModbusUtils.pushByte(queue, data.length);
+ queue.push(data);
+ }
+
+ @Override
+ ModbusResponse handleImpl(ProcessImage processImage) throws ModbusTransportException {
+ short[] sdata = convertToShorts(data);
+ for (int i = 0; i < sdata.length; i++)
+ processImage.writeHoldingRegister(startOffset + i, sdata[i]);
+ return new WriteRegistersResponse(slaveId, startOffset, sdata.length);
+ }
+
+ @Override
+ public byte getFunctionCode() {
+ return FunctionCode.WRITE_REGISTERS;
+ }
+
+ @Override
+ ModbusResponse getResponseInstance(int slaveId) throws ModbusTransportException {
+ return new WriteRegistersResponse(slaveId);
+ }
+
+ @Override
+ protected void readRequest(ByteQueue queue) {
+ startOffset = ModbusUtils.popUnsignedShort(queue);
+ ModbusUtils.popUnsignedShort(queue); // register count not needed.
+ data = new byte[ModbusUtils.popUnsignedByte(queue)];
+ queue.pop(data);
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/WriteRegistersResponse.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/WriteRegistersResponse.java
new file mode 100644
index 0000000..e28be38
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/msg/WriteRegistersResponse.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.ModbusUtils;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.FunctionCode;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * WriteRegistersResponse class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class WriteRegistersResponse extends ModbusResponse {
+ private int startOffset;
+ private int numberOfRegisters;
+
+ WriteRegistersResponse(int slaveId) throws ModbusTransportException {
+ super(slaveId);
+ }
+
+ WriteRegistersResponse(int slaveId, int startOffset, int numberOfRegisters) throws ModbusTransportException {
+ super(slaveId);
+ this.startOffset = startOffset;
+ this.numberOfRegisters = numberOfRegisters;
+ }
+
+ @Override
+ public byte getFunctionCode() {
+ return FunctionCode.WRITE_REGISTERS;
+ }
+
+ @Override
+ protected void writeResponse(ByteQueue queue) {
+ ModbusUtils.pushShort(queue, startOffset);
+ ModbusUtils.pushShort(queue, numberOfRegisters);
+ }
+
+ @Override
+ protected void readResponse(ByteQueue queue) {
+ startOffset = ModbusUtils.popUnsignedShort(queue);
+ numberOfRegisters = ModbusUtils.popUnsignedShort(queue);
+ }
+
+ /**
+ * Getter for the field startOffset.
+ *
+ * @return a int.
+ */
+ public int getStartOffset() {
+ return startOffset;
+ }
+
+ /**
+ * Getter for the field numberOfRegisters.
+ *
+ * @return a int.
+ */
+ public int getNumberOfRegisters() {
+ return numberOfRegisters;
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/SerialMaster.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/SerialMaster.java
new file mode 100644
index 0000000..0cc485a
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/SerialMaster.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.serial;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ModbusMaster;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusInitException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.EpollStreamTransport;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.MessageControl;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.StreamTransport;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.Transport;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Abstract SerialMaster class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+abstract public class SerialMaster extends ModbusMaster {
+
+ private static final int RETRY_PAUSE_START = 50;
+ private static final int RETRY_PAUSE_MAX = 1000;
+
+ private final Log LOG = LogFactory.getLog(SerialMaster.class);
+
+ // Runtime fields.
+ protected boolean serialPortOpen;
+ protected SerialPortWrapper wrapper;
+ protected Transport transport;
+
+
+ /**
+ * Constructor for SerialMaster.
+ *
+ * Default to validating the slave id in responses
+ *
+ * @param wrapper a {@link SerialPortWrapper} object.
+ */
+ public SerialMaster(SerialPortWrapper wrapper) {
+ this(wrapper, true);
+ }
+
+ /**
+ *
Constructor for SerialMaster.
+ *
+ * @param wrapper a {@link SerialPortWrapper} object.
+ * @param validateResponse - confirm that requested slave id is the same in the response
+ */
+ public SerialMaster(SerialPortWrapper wrapper, boolean validateResponse) {
+ this.wrapper = wrapper;
+ this.validateResponse = validateResponse;
+ }
+
+ @Override
+ public void init() throws ModbusInitException {
+ try {
+ this.openConnection(null);
+ } catch (Exception e) {
+ throw new ModbusInitException(e);
+ }
+ }
+
+ /**
+ * Open the serial port and initialize the transport, ensure
+ * connection is closed first
+ *
+ * @param conn
+ * @throws Exception
+ */
+ protected void openConnection(MessageControl toClose) throws Exception {
+ // Make sure any existing connection is closed.
+ closeConnection(toClose);
+
+ // Try 'retries' times to get the socket open.
+ int retries = getRetries();
+ int retryPause = RETRY_PAUSE_START;
+ while (true) {
+ try {
+ this.wrapper.open();
+ this.serialPortOpen = true;
+ if (getePoll() != null) {
+ transport = new EpollStreamTransport(wrapper.getInputStream(),
+ wrapper.getOutputStream(),
+ getePoll());
+ } else {
+ transport = new StreamTransport(wrapper.getInputStream(),
+ wrapper.getOutputStream());
+ }
+ break;
+ } catch (Exception e) {
+ //Ensure port is closed before we try to reopen or bail out
+ close();
+
+ if (retries <= 0)
+ throw e;
+
+ retries--;
+
+ // Pause for a bit.
+ try {
+ Thread.sleep(retryPause);
+ } catch (InterruptedException e1) {
+ // ignore
+ }
+ retryPause *= 2;
+ if (retryPause > RETRY_PAUSE_MAX)
+ retryPause = RETRY_PAUSE_MAX;
+ }
+ }
+ }
+
+ /**
+ * Close serial port
+ *
+ * @param conn
+ */
+ protected void closeConnection(MessageControl conn) {
+ closeMessageControl(conn);
+ try {
+ if (serialPortOpen) {
+ wrapper.close();
+ serialPortOpen = false;
+ }
+ } catch (Exception e) {
+ getExceptionHandler().receivedException(e);
+ }
+
+ transport = null;
+ }
+
+ /**
+ * close.
+ */
+ public void close() {
+ try {
+ wrapper.close();
+ } catch (Exception e) {
+ LOG.error(e.getMessage(), e);
+ }
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/SerialMessage.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/SerialMessage.java
new file mode 100644
index 0000000..454e6da
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/SerialMessage.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.serial;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusMessage;
+
+/**
+ * Abstract SerialMessage class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+abstract public class SerialMessage {
+ protected final ModbusMessage modbusMessage;
+
+ /**
+ * Constructor for SerialMessage.
+ *
+ * @param modbusMessage a {@link ModbusMessage} object.
+ */
+ public SerialMessage(ModbusMessage modbusMessage) {
+ this.modbusMessage = modbusMessage;
+ }
+
+ /**
+ * Getter for the field modbusMessage.
+ *
+ * @return a {@link ModbusMessage} object.
+ */
+ public ModbusMessage getModbusMessage() {
+ return modbusMessage;
+ }
+
+ @Override
+ public String toString() {
+ return "SerialMessage [modbusMessage=" + modbusMessage + "]";
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/SerialPortWrapper.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/SerialPortWrapper.java
new file mode 100644
index 0000000..2d3bbba
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/SerialPortWrapper.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.serial;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Wrapper to further aid in abstracting Modbus4J from a serial port implementation
+ *
+ * @author Terry Packer
+ * @version 2025.9.0
+ */
+public interface SerialPortWrapper {
+
+ /**
+ * Close the Serial Port
+ *
+ * @throws Exception if any.
+ */
+ void close() throws Exception;
+
+ /**
+ * open.
+ *
+ * @throws Exception if any.
+ */
+ void open() throws Exception;
+
+ /**
+ * Return the input stream for an open port
+ *
+ * @return a {@link InputStream} object.
+ */
+ InputStream getInputStream();
+
+ /**
+ * Return the output stream for an open port
+ *
+ * @return a {@link OutputStream} object.
+ */
+ OutputStream getOutputStream();
+
+ /**
+ * getBaudRate.
+ *
+ * @return a int.
+ */
+ int getBaudRate();
+
+ /**
+ * getDataBits.
+ *
+ * @return a int.
+ */
+ int getDataBits();
+
+ /**
+ * getStopBits.
+ *
+ * @return a int.
+ */
+ int getStopBits();
+
+ /**
+ * getParity.
+ *
+ * @return a int.
+ */
+ int getParity();
+
+
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/SerialSlave.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/SerialSlave.java
new file mode 100644
index 0000000..f21bc4c
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/SerialSlave.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.serial;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ModbusSlaveSet;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusInitException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.StreamTransport;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Abstract SerialSlave class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+abstract public class SerialSlave extends ModbusSlaveSet {
+
+ private final Log LOG = LogFactory.getLog(SerialSlave.class);
+ protected StreamTransport transport;
+ // Runtime fields
+ private SerialPortWrapper wrapper;
+
+ /**
+ * Constructor for SerialSlave.
+ *
+ * @param wrapper a {@link SerialPortWrapper} object.
+ */
+ public SerialSlave(SerialPortWrapper wrapper) {
+ this.wrapper = wrapper;
+ }
+
+ @Override
+ public void start() throws ModbusInitException {
+ try {
+
+ wrapper.open();
+
+ transport = new StreamTransport(wrapper.getInputStream(), wrapper.getOutputStream());
+ } catch (Exception e) {
+ throw new ModbusInitException(e);
+ }
+ }
+
+ @Override
+ public void stop() {
+ try {
+ wrapper.close();
+ } catch (Exception e) {
+ LOG.error(e.getMessage(), e);
+ }
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/SerialWaitingRoomKeyFactory.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/SerialWaitingRoomKeyFactory.java
new file mode 100644
index 0000000..776353c
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/SerialWaitingRoomKeyFactory.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.serial;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.IncomingResponseMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.OutgoingRequestMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.WaitingRoomKey;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.WaitingRoomKeyFactory;
+
+/**
+ * SerialWaitingRoomKeyFactory class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class SerialWaitingRoomKeyFactory implements WaitingRoomKeyFactory {
+ private static final Sync sync = new Sync();
+
+ @Override
+ public WaitingRoomKey createWaitingRoomKey(OutgoingRequestMessage request) {
+ return sync;
+ }
+
+ @Override
+ public WaitingRoomKey createWaitingRoomKey(IncomingResponseMessage response) {
+ return sync;
+ }
+
+ static class Sync implements WaitingRoomKey {
+ @Override
+ public int hashCode() {
+ return 1;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ return true;
+ }
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/ascii/AsciiMaster.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/ascii/AsciiMaster.java
new file mode 100644
index 0000000..7caaa3f
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/ascii/AsciiMaster.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.serial.ascii;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusInitException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusRequest;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusResponse;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.serial.SerialMaster;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.serial.SerialPortWrapper;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.serial.SerialWaitingRoomKeyFactory;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.MessageControl;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.StreamTransport;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * AsciiMaster class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class AsciiMaster extends SerialMaster {
+ private final Log LOG = LogFactory.getLog(SerialMaster.class);
+
+ private MessageControl conn;
+
+ /**
+ * Constructor for AsciiMaster.
+ *
+ * Default to validating the slave id in responses
+ *
+ * @param wrapper a {@link SerialPortWrapper} object.
+ */
+ public AsciiMaster(SerialPortWrapper wrapper) {
+ super(wrapper, true);
+ }
+
+ /**
+ * @param wrapper a {@link SerialPortWrapper} object.
+ * @param validateResponse - confirm that requested slave id is the same in the response
+ */
+ public AsciiMaster(SerialPortWrapper wrapper, boolean validateResponse) {
+ super(wrapper, validateResponse);
+ }
+
+ @Override
+ public void init() throws ModbusInitException {
+ try {
+ openConnection(null);
+ } catch (Exception e) {
+ throw new ModbusInitException(e);
+ }
+ initialized = true;
+ }
+
+ @Override
+ protected void openConnection(MessageControl toClose) throws Exception {
+ super.openConnection(toClose);
+ AsciiMessageParser asciiMessageParser = new AsciiMessageParser(true);
+ this.conn = getMessageControl();
+ this.conn.start(transport, asciiMessageParser, null, new SerialWaitingRoomKeyFactory());
+ if (getePoll() == null) {
+ ((StreamTransport) transport).start("Modbus ASCII master");
+ }
+ }
+
+ @Override
+ public void destroy() {
+ closeMessageControl(conn);
+ super.close();
+ initialized = false;
+ }
+
+ @Override
+ public ModbusResponse sendImpl(ModbusRequest request) throws ModbusTransportException {
+ // Wrap the modbus request in an ascii request.
+ AsciiMessageRequest asciiRequest = new AsciiMessageRequest(request);
+
+ // Send the request to get the response.
+ AsciiMessageResponse asciiResponse;
+ try {
+ asciiResponse = (AsciiMessageResponse) conn.send(asciiRequest);
+ if (asciiResponse == null)
+ return null;
+ return asciiResponse.getModbusResponse();
+ } catch (Exception e) {
+ try {
+ LOG.debug("Connection may have been reset. Attempting to re-open.");
+ openConnection(conn);
+ asciiResponse = (AsciiMessageResponse) conn.send(asciiRequest);
+ if (asciiResponse == null)
+ return null;
+ return asciiResponse.getModbusResponse();
+ } catch (Exception e2) {
+ closeConnection(conn);
+ LOG.debug("Failed to re-connect", e);
+ throw new ModbusTransportException(e2, request.getSlaveId());
+ }
+ }
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/ascii/AsciiMessage.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/ascii/AsciiMessage.java
new file mode 100644
index 0000000..ba7b573
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/ascii/AsciiMessage.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.serial.ascii;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.serial.SerialMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ *
Abstract AsciiMessage class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+abstract public class AsciiMessage extends SerialMessage {
+ private static final byte START = ':';
+ private static final byte[] END = {'\r', '\n'};
+ private static byte[] lookupAscii = {0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x41, 0x42, 0x43,
+ 0x44, 0x45, 0x46, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x33,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x43,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x45, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,};
+ private static byte[] lookupUnascii = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a,
+ 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,};
+
+ AsciiMessage(ModbusMessage modbusMessage) {
+ super(modbusMessage);
+ }
+
+ /**
+ * getUnasciiMessage.
+ *
+ * @param queue a {@link ByteQueue} object.
+ * @return a {@link ByteQueue} object.
+ * @throws ModbusTransportException if any.
+ */
+ protected static ByteQueue getUnasciiMessage(ByteQueue queue) throws ModbusTransportException {
+ // Validate that the message starts with the required indicator
+ byte b = queue.pop();
+ if (b != START)
+ throw new ModbusTransportException("Invalid message start: " + b);
+
+ // Find the end indicator
+ int end = queue.indexOf(END);
+ if (end == -1)
+ throw new ArrayIndexOutOfBoundsException();
+
+ // Remove the message from the queue, leaving the LRC there
+ byte[] asciiBytes = new byte[end - 2];
+ queue.pop(asciiBytes);
+ ByteQueue msgQueue = new ByteQueue(asciiBytes);
+
+ // Pop off the LRC
+ byte givenLrc = readAscii(queue);
+
+ // Pop the end indicator off of the queue
+ queue.pop(END.length);
+
+ // Convert to unascii
+ fromAscii(msgQueue, msgQueue.size());
+
+ // Check the LRC
+ int calcLrc = calculateLRC(msgQueue, 0, msgQueue.size());
+ if (calcLrc != givenLrc)
+ throw new ModbusTransportException("LRC mismatch: given=" + (givenLrc & 0xff) + ", calc="
+ + (calcLrc & 0xff));
+
+ return msgQueue;
+ }
+
+ private static byte calculateLRC(ByteQueue queue, int start, int len) {
+ int lrc = 0;
+ for (int i = 0; i < len; i++)
+ lrc -= queue.peek(i + start);
+ return (byte) (lrc & 0xff);
+ }
+
+ private static void toAscii(ByteQueue queue, int unasciiLen) {
+ for (int i = 0; i < unasciiLen; i++)
+ writeAscii(queue, queue.pop());
+ }
+
+ private static void writeAscii(ByteQueue to, byte b) {
+ to.push(lookupAscii[b & 0xf0]);
+ to.push(lookupAscii[b & 0x0f]);
+ }
+
+ private static void fromAscii(ByteQueue queue, int asciiLen) {
+ int len = asciiLen / 2;
+ for (int i = 0; i < len; i++)
+ queue.push(readAscii(queue));
+ }
+
+ private static byte readAscii(ByteQueue from) {
+ return (byte) ((lookupUnascii[from.pop()] << 4) | lookupUnascii[from.pop()]);
+ }
+
+ /**
+ * getAsciiData.
+ *
+ * @param queue a {@link ByteQueue} object.
+ * @return an array of {@link byte} objects.
+ */
+ protected byte[] getAsciiData(ByteQueue queue) {
+ int unasciiLen = queue.size();
+
+ // Convert the message to ascii
+ queue.push(START);
+ byte lrc = calculateLRC(queue, 0, unasciiLen);
+ toAscii(queue, unasciiLen);
+ writeAscii(queue, lrc);
+ queue.push(END);
+
+ // Return the data.
+ return queue.popAll();
+ }
+
+ /**
+ * getMessageData.
+ *
+ * @return an array of {@link byte} objects.
+ */
+ public byte[] getMessageData() {
+ ByteQueue queue = new ByteQueue();
+ modbusMessage.write(queue);
+ return getAsciiData(queue);
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/ascii/AsciiMessageParser.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/ascii/AsciiMessageParser.java
new file mode 100644
index 0000000..8fc6ebc
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/ascii/AsciiMessageParser.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.serial.ascii;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.BaseMessageParser;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.IncomingMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * AsciiMessageParser class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class AsciiMessageParser extends BaseMessageParser {
+ /**
+ * Constructor for AsciiMessageParser.
+ *
+ * @param master a boolean.
+ */
+ public AsciiMessageParser(boolean master) {
+ super(master);
+ }
+
+ @Override
+ protected IncomingMessage parseMessageImpl(ByteQueue queue) throws Exception {
+ if (master)
+ return AsciiMessageResponse.createAsciiMessageResponse(queue);
+ return AsciiMessageRequest.createAsciiMessageRequest(queue);
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/ascii/AsciiMessageRequest.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/ascii/AsciiMessageRequest.java
new file mode 100644
index 0000000..5510cd4
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/ascii/AsciiMessageRequest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.serial.ascii;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusRequest;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.IncomingRequestMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.OutgoingRequestMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * AsciiMessageRequest class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class AsciiMessageRequest extends AsciiMessage implements OutgoingRequestMessage, IncomingRequestMessage {
+ /**
+ * Constructor for AsciiMessageRequest.
+ *
+ * @param modbusMessage a {@link ModbusMessage} object.
+ */
+ public AsciiMessageRequest(ModbusMessage modbusMessage) {
+ super(modbusMessage);
+ }
+
+ static AsciiMessageRequest createAsciiMessageRequest(ByteQueue queue) throws ModbusTransportException {
+ ByteQueue msgQueue = getUnasciiMessage(queue);
+ ModbusRequest request = ModbusRequest.createModbusRequest(msgQueue);
+ AsciiMessageRequest asciiRequest = new AsciiMessageRequest(request);
+
+ // Return the data.
+ return asciiRequest;
+ }
+
+ @Override
+ public boolean expectsResponse() {
+ return modbusMessage.getSlaveId() != 0;
+ }
+
+ /**
+ * getModbusRequest.
+ *
+ * @return a {@link ModbusRequest} object.
+ */
+ public ModbusRequest getModbusRequest() {
+ return (ModbusRequest) modbusMessage;
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/ascii/AsciiMessageResponse.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/ascii/AsciiMessageResponse.java
new file mode 100644
index 0000000..2972cbf
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/ascii/AsciiMessageResponse.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.serial.ascii;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusResponse;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.IncomingResponseMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.OutgoingResponseMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * AsciiMessageResponse class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class AsciiMessageResponse extends AsciiMessage implements OutgoingResponseMessage, IncomingResponseMessage {
+ /**
+ * Constructor for AsciiMessageResponse.
+ *
+ * @param modbusMessage a {@link ModbusMessage} object.
+ */
+ public AsciiMessageResponse(ModbusMessage modbusMessage) {
+ super(modbusMessage);
+ }
+
+ static AsciiMessageResponse createAsciiMessageResponse(ByteQueue queue) throws ModbusTransportException {
+ ByteQueue msgQueue = getUnasciiMessage(queue);
+ ModbusResponse response = ModbusResponse.createModbusResponse(msgQueue);
+ AsciiMessageResponse asciiResponse = new AsciiMessageResponse(response);
+
+ // Return the data.
+ return asciiResponse;
+ }
+
+ /**
+ * getModbusResponse.
+ *
+ * @return a {@link ModbusResponse} object.
+ */
+ public ModbusResponse getModbusResponse() {
+ return (ModbusResponse) modbusMessage;
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/ascii/AsciiRequestHandler.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/ascii/AsciiRequestHandler.java
new file mode 100644
index 0000000..3ea7334
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/ascii/AsciiRequestHandler.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.serial.ascii;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ModbusSlaveSet;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.BaseRequestHandler;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusRequest;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusResponse;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.IncomingRequestMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.OutgoingResponseMessage;
+
+/**
+ * AsciiRequestHandler class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class AsciiRequestHandler extends BaseRequestHandler {
+ /**
+ * Constructor for AsciiRequestHandler.
+ *
+ * @param slave a {@link ModbusSlaveSet} object.
+ */
+ public AsciiRequestHandler(ModbusSlaveSet slave) {
+ super(slave);
+ }
+
+
+ public OutgoingResponseMessage handleRequest(IncomingRequestMessage req) throws Exception {
+ AsciiMessageRequest asciiRequest = (AsciiMessageRequest) req;
+ ModbusRequest request = asciiRequest.getModbusRequest();
+ ModbusResponse response = handleRequestImpl(request);
+ if (response == null)
+ return null;
+ return new AsciiMessageResponse(response);
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/ascii/AsciiSlave.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/ascii/AsciiSlave.java
new file mode 100644
index 0000000..aeda5d8
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/ascii/AsciiSlave.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.serial.ascii;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusInitException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.serial.SerialPortWrapper;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.serial.SerialSlave;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.MessageControl;
+
+import java.io.IOException;
+
+/**
+ * AsciiSlave class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class AsciiSlave extends SerialSlave {
+ private MessageControl conn;
+
+ /**
+ * Constructor for AsciiSlave.
+ *
+ * @param wrapper a {@link SerialPortWrapper} object.
+ */
+ public AsciiSlave(SerialPortWrapper wrapper) {
+ super(wrapper);
+ }
+
+ @Override
+ public void start() throws ModbusInitException {
+ super.start();
+
+ AsciiMessageParser asciiMessageParser = new AsciiMessageParser(false);
+ AsciiRequestHandler asciiRequestHandler = new AsciiRequestHandler(this);
+
+ conn = new MessageControl();
+ conn.setExceptionHandler(getExceptionHandler());
+
+ try {
+ conn.start(transport, asciiMessageParser, asciiRequestHandler, null);
+ transport.start("Modbus ASCII slave");
+ } catch (IOException e) {
+ throw new ModbusInitException(e);
+ }
+ }
+
+ @Override
+ public void stop() {
+ conn.close();
+ super.stop();
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/rtu/RtuMaster.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/rtu/RtuMaster.java
new file mode 100644
index 0000000..cd4fad2
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/rtu/RtuMaster.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.serial.rtu;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusInitException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusRequest;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusResponse;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.serial.SerialMaster;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.serial.SerialPortWrapper;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.serial.SerialWaitingRoomKeyFactory;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.ShouldNeverHappenException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.MessageControl;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.StreamTransport;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * RtuMaster class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class RtuMaster extends SerialMaster {
+
+ private final Log LOG = LogFactory.getLog(RtuMaster.class);
+
+ // Runtime fields.
+ private MessageControl conn;
+
+ /**
+ * Constructor for RtuMaster.
+ *
+ * Default to validating the slave id in responses
+ *
+ * @param wrapper a {@link SerialPortWrapper} object.
+ */
+ public RtuMaster(SerialPortWrapper wrapper) {
+ super(wrapper, true);
+ }
+
+ /**
+ *
Constructor for RtuMaster.
+ *
+ * @param wrapper a {@link SerialPortWrapper} object.
+ * @param validateResponse - confirm that requested slave id is the same in the response
+ */
+ public RtuMaster(SerialPortWrapper wrapper, boolean validateResponse) {
+ super(wrapper, validateResponse);
+ }
+
+ /**
+ * RTU Spec:
+ * For baud greater than 19200
+ * Message Spacing: 1.750uS
+ *
+ * For baud less than 19200
+ * Message Spacing: 3.5 * char time
+ *
+ * @param wrapper a {@link SerialPortWrapper} object.
+ * @return a long.
+ */
+ public static long computeMessageFrameSpacing(SerialPortWrapper wrapper) {
+ //For Modbus Serial Spec, Message Framing rates at 19200 Baud are fixed
+ if (wrapper.getBaudRate() > 19200) {
+ return 1750000l; //Nanoseconds
+ } else {
+ float charTime = computeCharacterTime(wrapper);
+ return (long) (charTime * 3.5f);
+ }
+ }
+
+ /**
+ * RTU Spec:
+ * For baud greater than 19200
+ * Char Spacing: 750uS
+ *
+ * For baud less than 19200
+ * Char Spacing: 1.5 * char time
+ *
+ * @param wrapper a {@link SerialPortWrapper} object.
+ * @return a long.
+ */
+ public static long computeCharacterSpacing(SerialPortWrapper wrapper) {
+ //For Modbus Serial Spec, Message Framing rates at 19200 Baud are fixed
+ if (wrapper.getBaudRate() > 19200) {
+ return 750000l; //Nanoseconds
+ } else {
+ float charTime = computeCharacterTime(wrapper);
+ return (long) (charTime * 1.5f);
+ }
+ }
+
+ /**
+ * Compute the time it takes to transmit 1 character with
+ * the provided Serial Parameters.
+ *
+ * RTU Spec:
+ * For baud greater than 19200
+ * Char Spacing: 750uS
+ * Message Spacing: 1.750uS
+ *
+ * For baud less than 19200
+ * Char Spacing: 1.5 * char time
+ * Message Spacing: 3.5 * char time
+ *
+ * @param wrapper a {@link SerialPortWrapper} object.
+ * @return time in nanoseconds
+ */
+ public static float computeCharacterTime(SerialPortWrapper wrapper) {
+ //Compute the char size
+ float charBits = wrapper.getDataBits();
+ switch (wrapper.getStopBits()) {
+ case 1:
+ //Strangely this results in 0 stop bits.. in JSSC code
+ break;
+ case 2:
+ charBits += 2f;
+ break;
+ case 3:
+ //1.5 stop bits
+ charBits += 1.5f;
+ break;
+ default:
+ throw new ShouldNeverHappenException("Unknown stop bit size: " + wrapper.getStopBits());
+ }
+
+ if (wrapper.getParity() > 0)
+ charBits += 1; //Add another if using parity
+
+ //Compute ns it takes to send one char
+ // ((charSize/symbols per second) ) * ns per second
+ return (charBits / wrapper.getBaudRate()) * 1000000000f;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void init() throws ModbusInitException {
+ try {
+ openConnection(null);
+ } catch (Exception e) {
+ throw new ModbusInitException(e);
+ }
+ initialized = true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void openConnection(MessageControl toClose) throws Exception {
+ super.openConnection(toClose);
+
+ RtuMessageParser rtuMessageParser = new RtuMessageParser(true);
+ this.conn = getMessageControl();
+ this.conn.start(transport, rtuMessageParser, null, new SerialWaitingRoomKeyFactory());
+ if (getePoll() == null) {
+ ((StreamTransport) transport).start("Modbus RTU master");
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void destroy() {
+ closeMessageControl(conn);
+ super.close();
+ initialized = false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public ModbusResponse sendImpl(ModbusRequest request) throws ModbusTransportException {
+ // Wrap the modbus request in an rtu request.
+ RtuMessageRequest rtuRequest = new RtuMessageRequest(request);
+
+ // Send the request to get the response.
+ RtuMessageResponse rtuResponse;
+ try {
+ rtuResponse = (RtuMessageResponse) conn.send(rtuRequest);
+ if (rtuResponse == null)
+ return null;
+ return rtuResponse.getModbusResponse();
+ } catch (Exception e) {
+ try {
+ LOG.debug("Connection may have been reset. Attempting to re-open.", e);
+ openConnection(conn);
+ rtuResponse = (RtuMessageResponse) conn.send(rtuRequest);
+ if (rtuResponse == null)
+ return null;
+ return rtuResponse.getModbusResponse();
+ } catch (Exception e2) {
+ closeConnection(conn);
+ LOG.debug("Failed to re-connect", e2);
+ throw new ModbusTransportException(e2, request.getSlaveId());
+ }
+ }
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/rtu/RtuMessage.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/rtu/RtuMessage.java
new file mode 100644
index 0000000..776f3b5
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/rtu/RtuMessage.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.serial.rtu;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.ModbusUtils;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.serial.SerialMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * Convenience superclass primarily for calculating CRC values.
+ *
+ * @author mlohbihler
+ * @version 2025.9.0
+ */
+public class RtuMessage extends SerialMessage {
+ /**
+ *
Constructor for RtuMessage.
+ *
+ * @param modbusMessage a {@link ModbusMessage} object.
+ */
+ public RtuMessage(ModbusMessage modbusMessage) {
+ super(modbusMessage);
+ }
+
+ /**
+ * getMessageData.
+ *
+ * @return an array of {@link byte} objects.
+ */
+ public byte[] getMessageData() {
+ ByteQueue queue = new ByteQueue();
+
+ // Write the particular message.
+ modbusMessage.write(queue);
+
+ // Write the CRC
+ ModbusUtils.pushShort(queue, ModbusUtils.calculateCRC(modbusMessage));
+
+ // Return the data.
+ return queue.popAll();
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/rtu/RtuMessageParser.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/rtu/RtuMessageParser.java
new file mode 100644
index 0000000..b10409c
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/rtu/RtuMessageParser.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.serial.rtu;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.BaseMessageParser;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.IncomingMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * Message parser implementation for RTU encoding. Primary reference for the ordering of CRC bytes. Also provides
+ * handling of incomplete messages.
+ *
+ * @author mlohbihler
+ * @version 2025.9.0
+ */
+public class RtuMessageParser extends BaseMessageParser {
+ /**
+ * Constructor for RtuMessageParser.
+ *
+ * @param master a boolean.
+ */
+ public RtuMessageParser(boolean master) {
+ super(master);
+ }
+
+ @Override
+ protected IncomingMessage parseMessageImpl(ByteQueue queue) throws Exception {
+ if (master)
+ return RtuMessageResponse.createRtuMessageResponse(queue);
+ return RtuMessageRequest.createRtuMessageRequest(queue);
+ }
+ //
+ // public static void main(String[] args) throws Exception {
+ // ByteQueue queue = new ByteQueue(new byte[] { 5, 3, 2, 0, (byte) 0xdc, (byte) 0x48, (byte) 0x1d, 0 });
+ // RtuMessageParser p = new RtuMessageParser(false);
+ // System.out.println(p.parseResponse(queue));
+ // }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/rtu/RtuMessageRequest.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/rtu/RtuMessageRequest.java
new file mode 100644
index 0000000..014c893
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/rtu/RtuMessageRequest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.serial.rtu;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.ModbusUtils;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusRequest;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.IncomingRequestMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.OutgoingRequestMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * Handles the RTU enveloping of modbus requests.
+ *
+ * @author mlohbihler
+ * @version 2025.9.0
+ */
+public class RtuMessageRequest extends RtuMessage implements OutgoingRequestMessage, IncomingRequestMessage {
+ /**
+ * Constructor for RtuMessageRequest.
+ *
+ * @param modbusRequest a {@link ModbusRequest} object.
+ */
+ public RtuMessageRequest(ModbusRequest modbusRequest) {
+ super(modbusRequest);
+ }
+
+ static RtuMessageRequest createRtuMessageRequest(ByteQueue queue) throws ModbusTransportException {
+ ModbusRequest request = ModbusRequest.createModbusRequest(queue);
+ RtuMessageRequest rtuRequest = new RtuMessageRequest(request);
+
+ // Check the CRC
+ ModbusUtils.checkCRC(rtuRequest.modbusMessage, queue);
+
+ // Return the data.
+ return rtuRequest;
+ }
+
+ @Override
+ public boolean expectsResponse() {
+ return modbusMessage.getSlaveId() != 0;
+ }
+
+ /**
+ * getModbusRequest.
+ *
+ * @return a {@link ModbusRequest} object.
+ */
+ public ModbusRequest getModbusRequest() {
+ return (ModbusRequest) modbusMessage;
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/rtu/RtuMessageResponse.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/rtu/RtuMessageResponse.java
new file mode 100644
index 0000000..a50194c
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/rtu/RtuMessageResponse.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.serial.rtu;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.ModbusUtils;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusTransportException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusResponse;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.IncomingResponseMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.OutgoingResponseMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * Handles the RTU enveloping of modbus responses.
+ *
+ * @author mlohbihler
+ * @version 2025.9.0
+ */
+public class RtuMessageResponse extends RtuMessage implements OutgoingResponseMessage, IncomingResponseMessage {
+ /**
+ * Constructor for RtuMessageResponse.
+ *
+ * @param modbusResponse a {@link ModbusResponse} object.
+ */
+ public RtuMessageResponse(ModbusResponse modbusResponse) {
+ super(modbusResponse);
+ }
+
+ static RtuMessageResponse createRtuMessageResponse(ByteQueue queue) throws ModbusTransportException {
+ ModbusResponse response = ModbusResponse.createModbusResponse(queue);
+ RtuMessageResponse rtuResponse = new RtuMessageResponse(response);
+
+ // Check the CRC
+ ModbusUtils.checkCRC(rtuResponse.modbusMessage, queue);
+
+ // Return the data.
+ return rtuResponse;
+ }
+
+ /**
+ * getModbusResponse.
+ *
+ * @return a {@link ModbusResponse} object.
+ */
+ public ModbusResponse getModbusResponse() {
+ return (ModbusResponse) modbusMessage;
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/rtu/RtuRequestHandler.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/rtu/RtuRequestHandler.java
new file mode 100644
index 0000000..4cad586
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/rtu/RtuRequestHandler.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.serial.rtu;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ModbusSlaveSet;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.base.BaseRequestHandler;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusRequest;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.msg.ModbusResponse;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.IncomingRequestMessage;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.OutgoingResponseMessage;
+
+/**
+ * RtuRequestHandler class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class RtuRequestHandler extends BaseRequestHandler {
+ /**
+ * Constructor for RtuRequestHandler.
+ *
+ * @param slave a {@link ModbusSlaveSet} object.
+ */
+ public RtuRequestHandler(ModbusSlaveSet slave) {
+ super(slave);
+ }
+
+
+ public OutgoingResponseMessage handleRequest(IncomingRequestMessage req) throws Exception {
+ RtuMessageRequest rtuRequest = (RtuMessageRequest) req;
+ ModbusRequest request = rtuRequest.getModbusRequest();
+ ModbusResponse response = handleRequestImpl(request);
+ if (response == null)
+ return null;
+ return new RtuMessageResponse(response);
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/rtu/RtuSlave.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/rtu/RtuSlave.java
new file mode 100644
index 0000000..3250470
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/serial/rtu/RtuSlave.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.serial.rtu;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.ModbusInitException;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.serial.SerialPortWrapper;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.serial.SerialSlave;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging.MessageControl;
+
+import java.io.IOException;
+
+/**
+ * RtuSlave class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class RtuSlave extends SerialSlave {
+ // Runtime fields
+ private MessageControl conn;
+
+ /**
+ * Constructor for RtuSlave.
+ *
+ * @param wrapper a {@link SerialPortWrapper} object.
+ */
+ public RtuSlave(SerialPortWrapper wrapper) {
+ super(wrapper);
+ }
+
+
+ @Override
+ public void start() throws ModbusInitException {
+ super.start();
+
+ RtuMessageParser rtuMessageParser = new RtuMessageParser(false);
+ RtuRequestHandler rtuRequestHandler = new RtuRequestHandler(this);
+
+ conn = new MessageControl();
+ conn.setExceptionHandler(getExceptionHandler());
+
+ try {
+ conn.start(transport, rtuMessageParser, rtuRequestHandler, null);
+ transport.start("Modbus RTU slave");
+ } catch (IOException e) {
+ throw new ModbusInitException(e);
+ }
+ }
+
+
+ @Override
+ public void stop() {
+ conn.close();
+ super.stop();
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/NotImplementedException.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/NotImplementedException.java
new file mode 100644
index 0000000..41065f9
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/NotImplementedException.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero;
+
+/**
+ * NotImplementedException class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class NotImplementedException extends RuntimeException {
+ private static final long serialVersionUID = -1;
+
+ /**
+ * Constructor for NotImplementedException.
+ */
+ public NotImplementedException() {
+ super();
+ }
+
+ /**
+ * Constructor for NotImplementedException.
+ *
+ * @param message a {@link String} object.
+ */
+ public NotImplementedException(String message) {
+ super(message);
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/ShouldNeverHappenException.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/ShouldNeverHappenException.java
new file mode 100644
index 0000000..b4bfc7b
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/ShouldNeverHappenException.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero;
+
+/**
+ * ShouldNeverHappenException class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class ShouldNeverHappenException extends RuntimeException {
+ private static final long serialVersionUID = -1;
+
+ /**
+ * Constructor for ShouldNeverHappenException.
+ *
+ * @param message a {@link String} object.
+ */
+ public ShouldNeverHappenException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructor for ShouldNeverHappenException.
+ *
+ * @param cause a {@link Throwable} object.
+ */
+ public ShouldNeverHappenException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/epoll/InputStreamEPollWrapper.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/epoll/InputStreamEPollWrapper.java
new file mode 100644
index 0000000..fec8a82
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/epoll/InputStreamEPollWrapper.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.epoll;
+
+import java.io.InputStream;
+
+/**
+ * InputStreamEPollWrapper interface.
+ *
+ * @author Terry Packer
+ * @version 2025.9.0
+ */
+public interface InputStreamEPollWrapper {
+
+ /**
+ * add.
+ *
+ * @param in a {@link InputStream} object.
+ * @param inputStreamCallback a {@link Modbus4JInputStreamCallback} object.
+ */
+ void add(InputStream in, Modbus4JInputStreamCallback inputStreamCallback);
+
+ /**
+ * remove.
+ *
+ * @param in a {@link InputStream} object.
+ */
+ void remove(InputStream in);
+
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/epoll/Modbus4JInputStreamCallback.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/epoll/Modbus4JInputStreamCallback.java
new file mode 100644
index 0000000..8b6cc6c
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/epoll/Modbus4JInputStreamCallback.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.epoll;
+
+import java.io.IOException;
+
+/**
+ * A callback interface for input streams.
+ *
+ * NOTE: if the InputStreamEPoll instance is terminated, any running processes will be destroyed without any
+ * notification to this callback.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public interface Modbus4JInputStreamCallback {
+ /**
+ * Called when content is read from the input stream.
+ *
+ * @param buf the content that was read. This is a shared byte array. Contents can be manipulated within this call,
+ * but the array itself should not be stored beyond the call since the contents will be changed.
+ * @param len the length of content that was read.
+ */
+ void input(byte[] buf, int len);
+
+ /**
+ * Called when the closure of the input stream is detected.
+ */
+ void closed();
+
+ /**
+ * Called if there is an {@link IOException} while reading input stream.
+ *
+ * @param e the exception that was received
+ */
+ void ioException(IOException e);
+
+ /**
+ * Called if the InputStreamEPoll instance was terminated while the input stream was still registered.
+ */
+ void terminated();
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/io/LineHandler.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/io/LineHandler.java
new file mode 100644
index 0000000..aa4696a
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/io/LineHandler.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.io;
+
+/**
+ *
LineHandler interface.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public interface LineHandler {
+ /**
+ * handleLine.
+ *
+ * @param line a {@link String} object.
+ */
+ public void handleLine(String line);
+
+ /**
+ * done.
+ */
+ public void done();
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/io/NullWriter.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/io/NullWriter.java
new file mode 100644
index 0000000..6fb0333
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/io/NullWriter.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.io;
+
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * NullWriter class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class NullWriter extends Writer {
+ @Override
+ public void write(char[] cbuf, int off, int len) throws IOException {
+ // no op
+ }
+
+ @Override
+ public void flush() throws IOException {
+ // no op
+ }
+
+ @Override
+ public void close() throws IOException {
+ // no op
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/io/StreamUtils.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/io/StreamUtils.java
new file mode 100644
index 0000000..d5b8673
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/io/StreamUtils.java
@@ -0,0 +1,634 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+/*
+ * Created on 1-Mar-2006
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.io;
+
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.*;
+import java.nio.ByteBuffer;
+import java.nio.channels.SocketChannel;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * StreamUtils class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class StreamUtils {
+ /**
+ * transfer.
+ *
+ * @param in a {@link InputStream} object.
+ * @param out a {@link OutputStream} object.
+ * @throws IOException if any.
+ */
+ public static void transfer(InputStream in, OutputStream out) throws IOException {
+ transfer(in, out, -1);
+ }
+
+ /**
+ * transfer.
+ *
+ * @param in a {@link InputStream} object.
+ * @param out a {@link OutputStream} object.
+ * @param limit a long.
+ * @throws IOException if any.
+ */
+ public static void transfer(InputStream in, OutputStream out, long limit) throws IOException {
+ byte[] buf = new byte[1024];
+ int readcount;
+ long total = 0;
+ while ((readcount = in.read(buf)) != -1) {
+ if (limit != -1) {
+ if (total + readcount > limit)
+ readcount = (int) (limit - total);
+ }
+
+ if (readcount > 0)
+ out.write(buf, 0, readcount);
+
+ total += readcount;
+ if (limit != -1 && total >= limit)
+ break;
+ }
+ out.flush();
+ }
+
+ /**
+ * transfer.
+ *
+ * @param in a {@link InputStream} object.
+ * @param out a {@link SocketChannel} object.
+ * @throws IOException if any.
+ */
+ public static void transfer(InputStream in, SocketChannel out) throws IOException {
+ byte[] buf = new byte[1024];
+ ByteBuffer bbuf = ByteBuffer.allocate(1024);
+ int len;
+ while ((len = in.read(buf)) != -1) {
+ bbuf.put(buf, 0, len);
+ bbuf.flip();
+ while (bbuf.remaining() > 0)
+ out.write(bbuf);
+ bbuf.clear();
+ }
+ }
+
+ /**
+ * transfer.
+ *
+ * @param reader a {@link Reader} object.
+ * @param writer a {@link Writer} object.
+ * @throws IOException if any.
+ */
+ public static void transfer(Reader reader, Writer writer) throws IOException {
+ transfer(reader, writer, -1);
+ }
+
+ /**
+ * transfer.
+ *
+ * @param reader a {@link Reader} object.
+ * @param writer a {@link Writer} object.
+ * @param limit a long.
+ * @throws IOException if any.
+ */
+ public static void transfer(Reader reader, Writer writer, long limit) throws IOException {
+ char[] buf = new char[1024];
+ int readcount;
+ long total = 0;
+ while ((readcount = reader.read(buf)) != -1) {
+ if (limit != -1) {
+ if (total + readcount > limit)
+ readcount = (int) (limit - total);
+ }
+
+ if (readcount > 0)
+ writer.write(buf, 0, readcount);
+
+ total += readcount;
+ if (limit != -1 && total >= limit)
+ break;
+ }
+ writer.flush();
+ }
+
+ /**
+ * read.
+ *
+ * @param in a {@link InputStream} object.
+ * @return an array of {@link byte} objects.
+ * @throws IOException if any.
+ */
+ public static byte[] read(InputStream in) throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream(in.available());
+ transfer(in, out);
+ return out.toByteArray();
+ }
+
+ /**
+ * read.
+ *
+ * @param reader a {@link Reader} object.
+ * @return an array of {@link char} objects.
+ * @throws IOException if any.
+ */
+ public static char[] read(Reader reader) throws IOException {
+ CharArrayWriter writer = new CharArrayWriter();
+ transfer(reader, writer);
+ return writer.toCharArray();
+ }
+
+ /**
+ * readChar.
+ *
+ * @param in a {@link InputStream} object.
+ * @return a char.
+ * @throws IOException if any.
+ */
+ public static char readChar(InputStream in) throws IOException {
+ return (char) in.read();
+ }
+
+ /**
+ * readString.
+ *
+ * @param in a {@link InputStream} object.
+ * @param length a int.
+ * @return a {@link String} object.
+ * @throws IOException if any.
+ */
+ public static String readString(InputStream in, int length) throws IOException {
+ StringBuilder sb = new StringBuilder(length);
+ for (int i = 0; i < length; i++)
+ sb.append(readChar(in));
+ return sb.toString();
+ }
+
+ /**
+ * readByte.
+ *
+ * @param in a {@link InputStream} object.
+ * @return a byte.
+ * @throws IOException if any.
+ */
+ public static byte readByte(InputStream in) throws IOException {
+ return (byte) in.read();
+ }
+
+ /**
+ * read4ByteSigned.
+ *
+ * @param in a {@link InputStream} object.
+ * @return a int.
+ * @throws IOException if any.
+ */
+ public static int read4ByteSigned(InputStream in) throws IOException {
+ return in.read() | (in.read() << 8) | (in.read() << 16) | (in.read() << 24);
+ }
+
+ /**
+ * read4ByteUnsigned.
+ *
+ * @param in a {@link InputStream} object.
+ * @return a long.
+ * @throws IOException if any.
+ */
+ public static long read4ByteUnsigned(InputStream in) throws IOException {
+ return in.read() | (in.read() << 8) | (in.read() << 16) | (in.read() << 24);
+ }
+
+ /**
+ * read2ByteUnsigned.
+ *
+ * @param in a {@link InputStream} object.
+ * @return a int.
+ * @throws IOException if any.
+ */
+ public static int read2ByteUnsigned(InputStream in) throws IOException {
+ return in.read() | (in.read() << 8);
+ }
+
+ /**
+ * read2ByteSigned.
+ *
+ * @param in a {@link InputStream} object.
+ * @return a short.
+ * @throws IOException if any.
+ */
+ public static short read2ByteSigned(InputStream in) throws IOException {
+ return (short) (in.read() | (in.read() << 8));
+ }
+
+ /**
+ * writeByte.
+ *
+ * @param out a {@link OutputStream} object.
+ * @param b a byte.
+ * @throws IOException if any.
+ */
+ public static void writeByte(OutputStream out, byte b) throws IOException {
+ out.write(b);
+ }
+
+ /**
+ * writeChar.
+ *
+ * @param out a {@link OutputStream} object.
+ * @param c a char.
+ * @throws IOException if any.
+ */
+ public static void writeChar(OutputStream out, char c) throws IOException {
+ out.write((byte) c);
+ }
+
+ /**
+ * writeString.
+ *
+ * @param out a {@link OutputStream} object.
+ * @param s a {@link String} object.
+ * @throws IOException if any.
+ */
+ public static void writeString(OutputStream out, String s) throws IOException {
+ for (int i = 0; i < s.length(); i++)
+ writeChar(out, s.charAt(i));
+ }
+
+ /**
+ * write4ByteSigned.
+ *
+ * @param out a {@link OutputStream} object.
+ * @param i a int.
+ * @throws IOException if any.
+ */
+ public static void write4ByteSigned(OutputStream out, int i) throws IOException {
+ out.write((byte) (i & 0xFF));
+ out.write((byte) ((i >> 8) & 0xFF));
+ out.write((byte) ((i >> 16) & 0xFF));
+ out.write((byte) ((i >> 24) & 0xFF));
+ }
+
+ /**
+ * write4ByteUnsigned.
+ *
+ * @param out a {@link OutputStream} object.
+ * @param l a long.
+ * @throws IOException if any.
+ */
+ public static void write4ByteUnsigned(OutputStream out, long l) throws IOException {
+ out.write((byte) (l & 0xFF));
+ out.write((byte) ((l >> 8) & 0xFF));
+ out.write((byte) ((l >> 16) & 0xFF));
+ out.write((byte) ((l >> 24) & 0xFF));
+ }
+
+ /**
+ * write2ByteUnsigned.
+ *
+ * @param out a {@link OutputStream} object.
+ * @param i a int.
+ * @throws IOException if any.
+ */
+ public static void write2ByteUnsigned(OutputStream out, int i) throws IOException {
+ out.write((byte) (i & 0xFF));
+ out.write((byte) ((i >> 8) & 0xFF));
+ }
+
+ /**
+ * write2ByteSigned.
+ *
+ * @param out a {@link OutputStream} object.
+ * @param s a short.
+ * @throws IOException if any.
+ */
+ public static void write2ByteSigned(OutputStream out, short s) throws IOException {
+ out.write((byte) (s & 0xFF));
+ out.write((byte) ((s >> 8) & 0xFF));
+ }
+
+ /**
+ * dumpArray.
+ *
+ * @param b an array of {@link byte} objects.
+ * @return a {@link String} object.
+ */
+ public static String dumpArray(byte[] b) {
+ return dumpArray(b, 0, b.length);
+ }
+
+ /**
+ * dumpArray.
+ *
+ * @param b an array of {@link byte} objects.
+ * @param pos a int.
+ * @param len a int.
+ * @return a {@link String} object.
+ */
+ public static String dumpArray(byte[] b, int pos, int len) {
+ StringBuilder sb = new StringBuilder();
+ sb.append('[');
+ for (int i = pos; i < len; i++) {
+ if (i > 0)
+ sb.append(",");
+ sb.append(b[i]);
+ }
+ sb.append(']');
+ return sb.toString();
+ }
+
+ /**
+ * dumpMessage.
+ *
+ * @param b an array of {@link byte} objects.
+ * @return a {@link String} object.
+ */
+ public static String dumpMessage(byte[] b) {
+ return dumpMessage(b, 0, b.length);
+ }
+
+ /**
+ * dumpMessage.
+ *
+ * @param b an array of {@link byte} objects.
+ * @param pos a int.
+ * @param len a int.
+ * @return a {@link String} object.
+ */
+ public static String dumpMessage(byte[] b, int pos, int len) {
+ StringBuilder sb = new StringBuilder();
+ sb.append('[');
+ for (int i = pos; i < len; i++) {
+ switch (b[i]) {
+ case 2:
+ sb.append("&STX;");
+ break;
+ case 3:
+ sb.append("&ETX;");
+ break;
+ case 27:
+ sb.append("&ESC;");
+ break;
+ default:
+ sb.append((char) b[i]);
+ }
+ }
+ sb.append(']');
+ return sb.toString();
+ }
+
+ /**
+ * dumpArrayHex.
+ *
+ * @param b an array of {@link byte} objects.
+ * @return a {@link String} object.
+ */
+ public static String dumpArrayHex(byte[] b) {
+ return dumpArrayHex(b, 0, b.length);
+ }
+
+ /**
+ * dumpArrayHex.
+ *
+ * @param b an array of {@link byte} objects.
+ * @param pos a int.
+ * @param len a int.
+ * @return a {@link String} object.
+ */
+ public static String dumpArrayHex(byte[] b, int pos, int len) {
+ StringBuilder sb = new StringBuilder();
+ sb.append('[');
+ for (int i = pos; i < len; i++) {
+ if (i > 0)
+ sb.append(",");
+ sb.append(Integer.toHexString(b[i] & 0xff));
+ }
+ sb.append(']');
+ return sb.toString();
+ }
+
+ /**
+ * dumpHex.
+ *
+ * @param b an array of {@link byte} objects.
+ * @return a {@link String} object.
+ */
+ public static String dumpHex(byte[] b) {
+ return dumpHex(b, 0, b.length);
+ }
+
+ /**
+ * dumpHex.
+ *
+ * @param b an array of {@link byte} objects.
+ * @param pos a int.
+ * @param len a int.
+ * @return a {@link String} object.
+ */
+ public static String dumpHex(byte[] b, int pos, int len) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = pos; i < len; i++)
+ sb.append(StringUtils.leftPad(Integer.toHexString(b[i] & 0xff), 2, '0'));
+ return sb.toString();
+ }
+
+ /**
+ * readFile.
+ *
+ * @param filename a {@link String} object.
+ * @return a {@link String} object.
+ * @throws IOException if any.
+ */
+ public static String readFile(String filename) throws IOException {
+ return readFile(new File(filename));
+ }
+
+ /**
+ * readFile.
+ *
+ * @param file a {@link File} object.
+ * @return a {@link String} object.
+ * @throws IOException if any.
+ */
+ public static String readFile(File file) throws IOException {
+ FileReader in = null;
+ try {
+ in = new FileReader(file);
+ StringWriter out = new StringWriter();
+ transfer(in, out);
+ return out.toString();
+ } finally {
+ if (in != null)
+ in.close();
+ }
+ }
+
+ /**
+ * readLines.
+ *
+ * @param filename a {@link String} object.
+ * @return a {@link List} object.
+ * @throws IOException if any.
+ */
+ public static List readLines(String filename) throws IOException {
+ return readLines(new File(filename));
+ }
+
+ /**
+ * readLines.
+ *
+ * @param file a {@link File} object.
+ * @return a {@link List} object.
+ * @throws IOException if any.
+ */
+ public static List readLines(File file) throws IOException {
+ List lines = new ArrayList();
+ BufferedReader in = null;
+ try {
+ in = new BufferedReader(new FileReader(file));
+ String line;
+ while ((line = in.readLine()) != null)
+ lines.add(line);
+ return lines;
+ } finally {
+ if (in != null)
+ in.close();
+ }
+ }
+
+ /**
+ * writeFile.
+ *
+ * @param filename a {@link String} object.
+ * @param content a {@link String} object.
+ * @throws IOException if any.
+ */
+ public static void writeFile(String filename, String content) throws IOException {
+ writeFile(new File(filename), content);
+ }
+
+ /**
+ * writeFile.
+ *
+ * @param file a {@link File} object.
+ * @param content a {@link String} object.
+ * @throws IOException if any.
+ */
+ public static void writeFile(File file, String content) throws IOException {
+ FileWriter out = null;
+ try {
+ out = new FileWriter(file);
+ out.write(content);
+ } finally {
+ if (out != null)
+ out.close();
+ }
+ }
+
+ /**
+ * readLines.
+ *
+ * @param filename a {@link String} object.
+ * @param lineHandler a {@link LineHandler} object.
+ * @throws IOException if any.
+ */
+ public static void readLines(String filename, LineHandler lineHandler) throws IOException {
+ BufferedReader in = null;
+ try {
+ in = new BufferedReader(new FileReader(filename));
+ String line;
+ while ((line = in.readLine()) != null)
+ lineHandler.handleLine(line);
+ lineHandler.done();
+ } finally {
+ if (in != null)
+ in.close();
+ }
+ }
+
+ /**
+ * toHex.
+ *
+ * @param bs an array of {@link byte} objects.
+ * @return a {@link String} object.
+ */
+ public static String toHex(byte[] bs) {
+ StringBuilder sb = new StringBuilder(bs.length * 2);
+ for (byte b : bs)
+ sb.append(StringUtils.leftPad(Integer.toHexString(b & 0xff), 2, '0'));
+ return sb.toString();
+ }
+
+ /**
+ * toHex.
+ *
+ * @param b a byte.
+ * @return a {@link String} object.
+ */
+ public static String toHex(byte b) {
+ return StringUtils.leftPad(Integer.toHexString(b & 0xff), 2, '0');
+ }
+
+ /**
+ * toHex.
+ *
+ * @param s a short.
+ * @return a {@link String} object.
+ */
+ public static String toHex(short s) {
+ return StringUtils.leftPad(Integer.toHexString(s & 0xffff), 4, '0');
+ }
+
+ /**
+ * toHex.
+ *
+ * @param i a int.
+ * @return a {@link String} object.
+ */
+ public static String toHex(int i) {
+ return StringUtils.leftPad(Integer.toHexString(i), 8, '0');
+ }
+
+ /**
+ * toHex.
+ *
+ * @param l a long.
+ * @return a {@link String} object.
+ */
+ public static String toHex(long l) {
+ return StringUtils.leftPad(Long.toHexString(l), 16, '0');
+ }
+
+ /**
+ * fromHex.
+ *
+ * @param s a {@link String} object.
+ * @return an array of {@link byte} objects.
+ */
+ public static byte[] fromHex(String s) {
+ byte[] bs = new byte[s.length() / 2];
+ for (int i = 0; i < bs.length; i++)
+ bs[i] = (byte) Integer.parseInt(s.substring(i * 2, i * 2 + 2), 16);
+ return bs;
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/log/BaseIOLog.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/log/BaseIOLog.java
new file mode 100644
index 0000000..a48a044
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/log/BaseIOLog.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.log;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.io.NullWriter;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.io.StreamUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/**
+ * Abstract BaseIOLog class.
+ *
+ * @author Terry Packer
+ * @version 2025.9.0
+ */
+public abstract class BaseIOLog {
+
+ /**
+ * Constant DATE_FORMAT="yyyy/MM/dd-HH:mm:ss,SSS"
+ */
+ protected static final String DATE_FORMAT = "yyyy/MM/dd-HH:mm:ss,SSS";
+ private static final Log LOG = LogFactory.getLog(BaseIOLog.class);
+ protected final SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT);
+ protected final File file;
+ protected final StringBuilder sb = new StringBuilder();
+ protected final Date date = new Date();
+ protected PrintWriter out;
+
+ /**
+ * Constructor for BaseIOLog.
+ *
+ * @param logFile a {@link File} object.
+ */
+ public BaseIOLog(File logFile) {
+ this.file = logFile;
+ createOut();
+ }
+
+ /**
+ * Create the Print Writer output
+ */
+ protected void createOut() {
+ try {
+ out = new PrintWriter(new FileWriter(file, true));
+ } catch (IOException e) {
+ out = new PrintWriter(new NullWriter());
+ LOG.error("Error while creating process log", e);
+ }
+ }
+
+ /**
+ * close.
+ */
+ public void close() {
+ out.close();
+ }
+
+ /**
+ * input.
+ *
+ * @param b an array of {@link byte} objects.
+ */
+ public void input(byte[] b) {
+ log(true, b, 0, b.length);
+ }
+
+ /**
+ * input.
+ *
+ * @param b an array of {@link byte} objects.
+ * @param pos a int.
+ * @param len a int.
+ */
+ public void input(byte[] b, int pos, int len) {
+ log(true, b, pos, len);
+ }
+
+ /**
+ * output.
+ *
+ * @param b an array of {@link byte} objects.
+ */
+ public void output(byte[] b) {
+ log(false, b, 0, b.length);
+ }
+
+ /**
+ * output.
+ *
+ * @param b an array of {@link byte} objects.
+ * @param pos a int.
+ * @param len a int.
+ */
+ public void output(byte[] b, int pos, int len) {
+ log(false, b, pos, len);
+ }
+
+ /**
+ * log.
+ *
+ * @param input a boolean.
+ * @param b an array of {@link byte} objects.
+ */
+ public void log(boolean input, byte[] b) {
+ log(input, b, 0, b.length);
+ }
+
+ /**
+ * log.
+ *
+ * @param input a boolean.
+ * @param b an array of {@link byte} objects.
+ * @param pos a int.
+ * @param len a int.
+ */
+ public synchronized void log(boolean input, byte[] b, int pos, int len) {
+ sizeCheck();
+
+ sb.delete(0, sb.length());
+ date.setTime(System.currentTimeMillis());
+ sb.append(sdf.format(date)).append(" ");
+ sb.append(input ? "I" : "O").append(" ");
+ sb.append(StreamUtils.dumpHex(b, pos, len));
+ out.println(sb.toString());
+ out.flush();
+ }
+
+ /**
+ * log.
+ *
+ * @param message a {@link String} object.
+ */
+ public synchronized void log(String message) {
+ sizeCheck();
+
+ sb.delete(0, sb.length());
+ date.setTime(System.currentTimeMillis());
+ sb.append(sdf.format(date)).append(" ");
+ sb.append(message);
+ out.println(sb.toString());
+ out.flush();
+ }
+
+ /**
+ * Check the size of the logfile and perform adjustments
+ * as necessary
+ */
+ protected abstract void sizeCheck();
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/log/IOLog.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/log/IOLog.java
new file mode 100644
index 0000000..32c8963
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/log/IOLog.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.log;
+
+import java.io.File;
+
+/**
+ * IOLog class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class IOLog extends BaseIOLog {
+ //private static final Log LOG = LogFactory.getLog(IOLog.class);
+ private static final int MAX_FILESIZE = 1000000;
+ // private static final int MAX_FILESIZE = 1000;
+ private final File backupFile;
+
+ /**
+ * Constructor for IOLog.
+ *
+ * @param filename a {@link String} object.
+ */
+ public IOLog(String filename) {
+ super(new File(filename));
+ backupFile = new File(filename + ".1");
+ }
+
+
+ @Override
+ protected void sizeCheck() {
+ // Check if the file should be rolled.
+ if (file.length() > MAX_FILESIZE) {
+ out.close();
+
+ if (backupFile.exists())
+ backupFile.delete();
+ file.renameTo(backupFile);
+ createOut();
+ }
+ }
+ //
+ // public static void main(String[] args) {
+ // byte[] b = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
+ //
+ // IOLog log = new IOLog("iotest");
+ // log.log("test");
+ // log.log("testtest");
+ //
+ // log.input(b);
+ // log.output(b);
+ // log.input(b);
+ // log.output(b);
+ // log.input(b);
+ // log.output(b);
+ // log.input(b);
+ // log.output(b);
+ // log.input(b);
+ // log.output(b);
+ // log.input(b);
+ // log.output(b);
+ // log.input(b);
+ // log.output(b);
+ // log.input(b);
+ // log.output(b);
+ // log.input(b);
+ // log.output(b);
+ // log.input(b);
+ // log.output(b);
+ // log.input(b);
+ // log.output(b);
+ // log.input(b);
+ // log.output(b);
+ //
+ // log.log("testtesttesttesttesttesttesttesttesttest");
+ // }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/log/RollingIOLog.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/log/RollingIOLog.java
new file mode 100644
index 0000000..94a8228
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/log/RollingIOLog.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.log;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+
+/**
+ * Class to Log IO with the option to keep historical files
+ *
+ * @author Terry Packer
+ * @version 2025.9.0
+ */
+public class RollingIOLog extends BaseIOLog {
+
+ private static final Log LOG = LogFactory.getLog(RollingIOLog.class);
+
+ //New Members
+ protected int fileSize;
+ protected int maxFiles;
+ protected int currentFileNumber;
+
+ /**
+ * Constructor for RollingIOLog.
+ *
+ * @param baseFilename - The base filename for all logfiles ie. dataLog.log
+ * @param logDirectory a {@link File} object.
+ * @param fileSize - in bytes of file before rolling over
+ * @param maxFiles - max number to keep in addition to the current log file
+ */
+ public RollingIOLog(final String baseFilename, File logDirectory, int fileSize, int maxFiles) {
+ super(new File(logDirectory, baseFilename)); //Ignoring this
+ createOut();
+
+ //Detect the current file number
+ File[] files = logDirectory.listFiles(new LogFilenameFilter(baseFilename));
+
+ //files will contain baseFilename.log, baseFilename.log.1 ... baseFilename.log.n
+ // where n is our currentFileNumber
+ this.currentFileNumber = files.length - 1;
+ if (this.currentFileNumber > maxFiles)
+ this.currentFileNumber = maxFiles;
+
+ this.fileSize = fileSize;
+ this.maxFiles = maxFiles;
+
+ }
+
+
+ @Override
+ protected void sizeCheck() {
+ // Check if the file should be rolled.
+ if (file.length() > this.fileSize) {
+ out.close();
+
+ try {
+ //Do rollover
+
+ for (int i = this.currentFileNumber; i > 0; i--) {
+ Path source = Paths.get(this.file.getAbsolutePath() + "." + i);
+ Path target = Paths.get(this.file.getAbsolutePath() + "." + (i + 1));
+ Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);
+ }
+
+ Path source = Paths.get(this.file.toURI());
+ Path target = Paths.get(this.file.getAbsolutePath() + "." + 1);
+ Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);
+
+ if (this.currentFileNumber < this.maxFiles - 1) {
+ //Use file number
+ this.currentFileNumber++;
+ }
+
+ } catch (IOException e) {
+ LOG.error(e);
+ }
+
+ createOut();
+ }
+ }
+
+
+ /**
+ * Class to filter log filenames from a directory listing
+ *
+ * @author Terry Packer
+ */
+ class LogFilenameFilter implements FilenameFilter {
+
+ private String nameToMatch;
+
+ public LogFilenameFilter(String nameToMatch) {
+ this.nameToMatch = nameToMatch;
+ }
+
+ @Override
+ public boolean accept(File dir, String name) {
+ return name.contains(this.nameToMatch);
+ }
+
+ }
+
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/log/SimpleLog.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/log/SimpleLog.java
new file mode 100644
index 0000000..e109c88
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/log/SimpleLog.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.log;
+
+import java.io.PrintWriter;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/**
+ * SimpleLog class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class SimpleLog {
+ private final PrintWriter out;
+ private final SimpleDateFormat sdf = new SimpleDateFormat("MM/dd HH:mm:ss.SSS");
+ private final StringBuilder sb = new StringBuilder();
+ private final Date date = new Date();
+
+ /**
+ * Constructor for SimpleLog.
+ */
+ public SimpleLog() {
+ this(new PrintWriter(System.out));
+ }
+
+ /**
+ * Constructor for SimpleLog.
+ *
+ * @param out a {@link PrintWriter} object.
+ */
+ public SimpleLog(PrintWriter out) {
+ this.out = out;
+ }
+
+ /**
+ * out.
+ *
+ * @param message a {@link String} object.
+ */
+ public void out(String message) {
+ out(message, null);
+ }
+
+ /**
+ * out.
+ *
+ * @param t a {@link Throwable} object.
+ */
+ public void out(Throwable t) {
+ out(null, t);
+ }
+
+ /**
+ * out.
+ *
+ * @param o a {@link Object} object.
+ */
+ public void out(Object o) {
+ if (o instanceof Throwable)
+ out(null, (Throwable) o);
+ else if (o == null)
+ out(null, null);
+ else
+ out(o.toString(), null);
+ }
+
+ /**
+ * close.
+ */
+ public void close() {
+ out.close();
+ }
+
+ /**
+ * out.
+ *
+ * @param message a {@link String} object.
+ * @param t a {@link Throwable} object.
+ */
+ public synchronized void out(String message, Throwable t) {
+ sb.delete(0, sb.length());
+ date.setTime(System.currentTimeMillis());
+ sb.append(sdf.format(date)).append(" ");
+ if (message != null)
+ sb.append(message);
+ if (t != null) {
+ if (t.getMessage() != null)
+ sb.append(" - ").append(t.getMessage());
+ out.println(sb.toString());
+ t.printStackTrace(out);
+ } else
+ out.println(sb.toString());
+ out.flush();
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/DataConsumer.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/DataConsumer.java
new file mode 100644
index 0000000..e611967
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/DataConsumer.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging;
+
+import java.io.IOException;
+
+/**
+ * DataConsumer interface.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public interface DataConsumer {
+ /**
+ * Notifies the consumer that new data is available
+ *
+ * @param b array of bytes representing the incoming information
+ * @param len length of the data
+ */
+ public void data(byte[] b, int len);
+
+ /**
+ * handleIOException.
+ *
+ * @param e a {@link IOException} object.
+ */
+ public void handleIOException(IOException e);
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/DefaultMessagingExceptionHandler.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/DefaultMessagingExceptionHandler.java
new file mode 100644
index 0000000..fd96188
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/DefaultMessagingExceptionHandler.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging;
+
+/**
+ * DefaultMessagingExceptionHandler class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class DefaultMessagingExceptionHandler implements MessagingExceptionHandler {
+
+ public void receivedException(Exception e) {
+ e.printStackTrace();
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/EpollStreamTransport.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/EpollStreamTransport.java
new file mode 100644
index 0000000..4079cce
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/EpollStreamTransport.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.epoll.InputStreamEPollWrapper;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.epoll.Modbus4JInputStreamCallback;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * First, instatiate with the streams and epoll. Then add a data consumer, or create a message control and pass this as
+ * the transport (which will make the message control the data consumer). Stop the transport by stopping the message
+ * control).
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class EpollStreamTransport implements Transport {
+ private final OutputStream out;
+ private final InputStream in;
+ private final InputStreamEPollWrapper epoll;
+
+ /**
+ * Constructor for EpollStreamTransport.
+ *
+ * @param in a {@link InputStream} object.
+ * @param out a {@link OutputStream} object.
+ * @param epoll a {@link InputStreamEPollWrapper} object.
+ */
+ public EpollStreamTransport(InputStream in, OutputStream out, InputStreamEPollWrapper epoll) {
+ this.out = out;
+ this.in = in;
+ this.epoll = epoll;
+ }
+
+ @Override
+ public void setConsumer(final DataConsumer consumer) {
+ epoll.add(in, new Modbus4JInputStreamCallback() {
+ @Override
+ public void terminated() {
+ removeConsumer();
+ }
+
+ @Override
+ public void ioException(IOException e) {
+ consumer.handleIOException(e);
+ }
+
+ @Override
+ public void input(byte[] buf, int len) {
+ consumer.data(buf, len);
+ }
+
+ @Override
+ public void closed() {
+ removeConsumer();
+ }
+ });
+ }
+
+ /**
+ * removeConsumer.
+ */
+ @Override
+ public void removeConsumer() {
+ epoll.remove(in);
+ }
+
+ /**
+ * write.
+ *
+ * @param data an array of {@link byte} objects.
+ * @throws IOException if any.
+ */
+ @Override
+ public void write(byte[] data) throws IOException {
+ out.write(data);
+ out.flush();
+ }
+
+ @Override
+ public void write(byte[] data, int len) throws IOException {
+ out.write(data, 0, len);
+ out.flush();
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/EpollStreamTransportCharSpaced.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/EpollStreamTransportCharSpaced.java
new file mode 100644
index 0000000..7ccb123
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/EpollStreamTransportCharSpaced.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.epoll.InputStreamEPollWrapper;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * EpollStreamTransportCharSpaced class.
+ *
+ * @author Terry Packer
+ * @version 2025.9.0
+ */
+public class EpollStreamTransportCharSpaced extends EpollStreamTransport {
+
+ private final long charSpacing; //Spacing for chars in nanoseconds
+ private final OutputStream out; //Since the subclass has private members
+
+ /**
+ * Constructor for EpollStreamTransportCharSpaced.
+ *
+ * @param in a {@link InputStream} object.
+ * @param out a {@link OutputStream} object.
+ * @param epoll a {@link InputStreamEPollWrapper} object.
+ * @param charSpacing a long.
+ */
+ public EpollStreamTransportCharSpaced(InputStream in, OutputStream out,
+ InputStreamEPollWrapper epoll, long charSpacing) {
+ super(in, out, epoll);
+ this.out = out;
+ this.charSpacing = charSpacing;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Perform a write, ensure space between chars
+ */
+ @Override
+ public void write(byte[] data) throws IOException {
+
+ try {
+ long waited = 0, writeStart, writeEnd, waitRemaining;
+ for (byte b : data) {
+ writeStart = System.nanoTime();
+ out.write(b);
+ writeEnd = System.nanoTime();
+ waited = writeEnd - writeStart;
+ if (waited < this.charSpacing) {
+ waitRemaining = this.charSpacing - waited;
+ Thread.sleep(waitRemaining / 1000000, (int) (waitRemaining % 1000000));
+ }
+
+ }
+ } catch (Exception e) {
+ throw new IOException(e);
+ }
+ out.flush();
+ }
+
+
+ public void write(byte[] data, int len) throws IOException {
+ try {
+ long waited = 0, writeStart, writeEnd, waitRemaining;
+ for (int i = 0; i < len; i++) {
+ writeStart = System.nanoTime();
+ out.write(data[i]);
+ writeEnd = System.nanoTime();
+ waited = writeEnd - writeStart;
+ if (waited < this.charSpacing) {
+ waitRemaining = this.charSpacing - waited;
+ Thread.sleep(waitRemaining / 1000000, (int) (waitRemaining % 1000000));
+ }
+
+ }
+ } catch (Exception e) {
+ throw new IOException(e);
+ }
+ out.flush();
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/IncomingMessage.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/IncomingMessage.java
new file mode 100644
index 0000000..86165d7
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/IncomingMessage.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging;
+
+/**
+ *
IncomingMessage interface.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public interface IncomingMessage {
+ // A marker interface
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/IncomingRequestMessage.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/IncomingRequestMessage.java
new file mode 100644
index 0000000..e3d6b4f
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/IncomingRequestMessage.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging;
+
+
+/**
+ * IncomingRequestMessage interface.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public interface IncomingRequestMessage extends IncomingMessage {
+ // A marker interface.
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/IncomingResponseMessage.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/IncomingResponseMessage.java
new file mode 100644
index 0000000..1bb5170
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/IncomingResponseMessage.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging;
+
+
+/**
+ * IncomingResponseMessage interface.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public interface IncomingResponseMessage extends IncomingMessage {
+ // A marker interface
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/InputStreamListener.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/InputStreamListener.java
new file mode 100644
index 0000000..f1ab707
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/InputStreamListener.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging;
+
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * This class provides a stoppable listener for an input stream that sends arbitrary information. A read() call to an
+ * input stream will typically not return as long as the stream is not sending any data. This class provides a way for
+ * stream listeners to safely listen and still respond when they are told to stop.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class InputStreamListener implements Runnable {
+ private static final int DEFAULT_READ_DELAY = 50;
+
+ private final InputStream in;
+ private final DataConsumer consumer;
+ private volatile boolean running = true;
+
+ /**
+ * Defaulted to 20ms, this value tells the listener how long to wait between polls. Setting this to very small
+ * values (as low as 1ms) can result in high processor consumption, but better responsiveness when data arrives in
+ * the stream. Very high values have the opposite effect.
+ */
+ private int readDelay = DEFAULT_READ_DELAY;
+
+ /**
+ * Constructor for InputStreamListener.
+ *
+ * @param in a {@link InputStream} object.
+ * @param consumer a {@link DataConsumer} object.
+ */
+ public InputStreamListener(InputStream in, DataConsumer consumer) {
+ this.in = in;
+ this.consumer = consumer;
+ }
+
+ /**
+ * Getter for the field readDelay.
+ *
+ * @return a int.
+ */
+ public int getReadDelay() {
+ return readDelay;
+ }
+
+ /**
+ * Setter for the field readDelay.
+ *
+ * @param readDelay a int.
+ */
+ public void setReadDelay(int readDelay) {
+ if (readDelay < 1)
+ throw new IllegalArgumentException("readDelay can't be less than one");
+ this.readDelay = readDelay;
+ }
+
+ /**
+ * start.
+ *
+ * @param threadName a {@link String} object.
+ */
+ public void start(String threadName) {
+ Thread thread = new Thread(this, threadName);
+ thread.setDaemon(true);
+ thread.start();
+ }
+
+ /**
+ * stop.
+ */
+ public void stop() {
+ running = false;
+ synchronized (this) {
+ notify();
+ }
+ }
+
+ /**
+ * run.
+ */
+ public void run() {
+ byte[] buf = new byte[1024];
+ int readcount;
+ try {
+ while (running) {
+ try {
+ if (in.available() == 0) {
+ synchronized (this) {
+ try {
+ wait(readDelay);
+ } catch (InterruptedException e) {
+ // no op
+ }
+ }
+ continue;
+ }
+
+ readcount = in.read(buf);
+ consumer.data(buf, readcount);
+ } catch (IOException e) {
+ consumer.handleIOException(e);
+ if (e.getMessage().equals("Stream closed."))
+ break;
+ if (e.getMessage().contains("nativeavailable"))
+ break;
+ }
+ }
+ } finally {
+ running = false;
+ }
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/MessageControl.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/MessageControl.java
new file mode 100644
index 0000000..a36297d
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/MessageControl.java
@@ -0,0 +1,329 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.io.StreamUtils;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.log.BaseIOLog;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.timer.SystemTimeSource;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.timer.TimeSource;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+import java.io.IOException;
+
+/**
+ * In general there are three messaging activities:
+ *
+ * - Send a message for which no reply is expected, e.g. a broadcast.
+ * - Send a message and wait for a response with timeout and retries.
+ * - Listen for unsolicited requests.
+ *
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class MessageControl implements DataConsumer {
+ private static int DEFAULT_RETRIES = 2;
+ private static int DEFAULT_TIMEOUT = 500;
+ private final WaitingRoom waitingRoom = new WaitingRoom();
+ private final ByteQueue dataBuffer = new ByteQueue();
+ public boolean DEBUG = false;
+ private Transport transport;
+ private MessageParser messageParser;
+ private RequestHandler requestHandler;
+ private WaitingRoomKeyFactory waitingRoomKeyFactory;
+ private MessagingExceptionHandler exceptionHandler = new DefaultMessagingExceptionHandler();
+ private int retries = DEFAULT_RETRIES;
+ private int timeout = DEFAULT_TIMEOUT;
+ private int discardDataDelay = 0;
+ private long lastDataTimestamp;
+ private BaseIOLog ioLog;
+ private TimeSource timeSource = new SystemTimeSource();
+
+ /**
+ * start.
+ *
+ * @param transport a {@link com.serotonin.modbus4j.sero.messaging.Transport} object.
+ * @param messageParser a {@link com.serotonin.modbus4j.sero.messaging.MessageParser} object.
+ * @param handler a {@link com.serotonin.modbus4j.sero.messaging.RequestHandler} object.
+ * @param waitingRoomKeyFactory a {@link com.serotonin.modbus4j.sero.messaging.WaitingRoomKeyFactory} object.
+ * @throws IOException if any.
+ */
+ public void start(Transport transport, MessageParser messageParser, RequestHandler handler,
+ WaitingRoomKeyFactory waitingRoomKeyFactory) throws IOException {
+ if (transport == null) {
+ throw new IllegalArgumentException("transport can't be null");
+ }
+ this.transport = transport;
+ this.messageParser = messageParser;
+ this.requestHandler = handler;
+ this.waitingRoomKeyFactory = waitingRoomKeyFactory;
+ waitingRoom.setKeyFactory(waitingRoomKeyFactory);
+ transport.setConsumer(this);
+ }
+
+ /**
+ * close.
+ */
+ public void close() {
+ if (transport != null) {
+ transport.removeConsumer();
+ }
+ }
+
+ /**
+ * Setter for the field exceptionHandler.
+ *
+ * @param exceptionHandler a {@link com.serotonin.modbus4j.sero.messaging.MessagingExceptionHandler} object.
+ */
+ public void setExceptionHandler(MessagingExceptionHandler exceptionHandler) {
+ if (exceptionHandler == null)
+ this.exceptionHandler = new DefaultMessagingExceptionHandler();
+ else
+ this.exceptionHandler = exceptionHandler;
+ }
+
+ /**
+ * Getter for the field retries.
+ *
+ * @return a int.
+ */
+ public int getRetries() {
+ return retries;
+ }
+
+ /**
+ * Setter for the field retries.
+ *
+ * @param retries a int.
+ */
+ public void setRetries(int retries) {
+ this.retries = retries;
+ }
+
+ /**
+ * Getter for the field timeout.
+ *
+ * @return a int.
+ */
+ public int getTimeout() {
+ return timeout;
+ }
+
+ /**
+ * Setter for the field timeout.
+ *
+ * @param timeout a int.
+ */
+ public void setTimeout(int timeout) {
+ this.timeout = timeout;
+ }
+
+ /**
+ * Getter for the field discardDataDelay.
+ *
+ * @return a int.
+ */
+ public int getDiscardDataDelay() {
+ return discardDataDelay;
+ }
+
+ /**
+ * Setter for the field discardDataDelay.
+ *
+ * @param discardDataDelay a int.
+ */
+ public void setDiscardDataDelay(int discardDataDelay) {
+ this.discardDataDelay = discardDataDelay;
+ }
+
+ /**
+ * Getter for the field ioLog.
+ *
+ * @return a {@link com.serotonin.modbus4j.sero.log.BaseIOLog} object.
+ */
+ public BaseIOLog getIoLog() {
+ return ioLog;
+ }
+
+ /**
+ * Setter for the field ioLog.
+ *
+ * @param ioLog a {@link com.serotonin.modbus4j.sero.log.BaseIOLog} object.
+ */
+ public void setIoLog(BaseIOLog ioLog) {
+ this.ioLog = ioLog;
+ }
+
+ /**
+ * Getter for the field timeSource.
+ *
+ * @return a {@link com.serotonin.modbus4j.sero.timer.TimeSource} object.
+ */
+ public TimeSource getTimeSource() {
+ return timeSource;
+ }
+
+ /**
+ * Setter for the field timeSource.
+ *
+ * @param timeSource a {@link com.serotonin.modbus4j.sero.timer.TimeSource} object.
+ */
+ public void setTimeSource(TimeSource timeSource) {
+ this.timeSource = timeSource;
+ }
+
+ /**
+ * send.
+ *
+ * @param request a {@link com.serotonin.modbus4j.sero.messaging.OutgoingRequestMessage} object.
+ * @return a {@link com.serotonin.modbus4j.sero.messaging.IncomingResponseMessage} object.
+ * @throws IOException if any.
+ */
+ public IncomingResponseMessage send(OutgoingRequestMessage request) throws IOException {
+ return send(request, timeout, retries);
+ }
+
+ /**
+ * send.
+ *
+ * @param request a {@link com.serotonin.modbus4j.sero.messaging.OutgoingRequestMessage} object.
+ * @param timeout a int.
+ * @param retries a int.
+ * @return a {@link com.serotonin.modbus4j.sero.messaging.IncomingResponseMessage} object.
+ * @throws IOException if any.
+ */
+ public IncomingResponseMessage send(OutgoingRequestMessage request, int timeout, int retries) throws IOException {
+ byte[] data = request.getMessageData();
+ if (DEBUG)
+ System.out.println("MessagingControl.send: " + StreamUtils.dumpHex(data));
+
+ IncomingResponseMessage response = null;
+
+ if (request.expectsResponse()) {
+ WaitingRoomKey key = waitingRoomKeyFactory.createWaitingRoomKey(request);
+
+ // Enter the waiting room
+ waitingRoom.enter(key);
+
+ try {
+ do {
+ // Send the request.
+ write(data);
+
+ // Wait for the response.
+ response = waitingRoom.getResponse(key, timeout);
+
+ if (DEBUG && response == null)
+ System.out.println("Timeout waiting for response");
+ }
+ while (response == null && retries-- > 0);
+ } finally {
+ // Leave the waiting room.
+ waitingRoom.leave(key);
+ }
+
+ if (response == null)
+ throw new TimeoutException("request=" + request);
+ } else
+ write(data);
+
+ return response;
+ }
+
+ /**
+ * send.
+ *
+ * @param response a {@link com.serotonin.modbus4j.sero.messaging.OutgoingResponseMessage} object.
+ * @throws IOException if any.
+ */
+ public void send(OutgoingResponseMessage response) throws IOException {
+ write(response.getMessageData());
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Incoming data from the transport. Single-threaded.
+ */
+ public void data(byte[] b, int len) {
+ if (DEBUG)
+ System.out.println("MessagingConnection.read: " + StreamUtils.dumpHex(b, 0, len));
+ if (ioLog != null)
+ ioLog.input(b, 0, len);
+
+ if (discardDataDelay > 0) {
+ long now = timeSource.currentTimeMillis();
+ if (now - lastDataTimestamp > discardDataDelay)
+ dataBuffer.clear();
+ lastDataTimestamp = now;
+ }
+
+ dataBuffer.push(b, 0, len);
+
+ // There may be multiple messages in the data, so enter a loop.
+ while (true) {
+ // Attempt to parse a message.
+ try {
+ // Mark where we are in the buffer. The entire message may not be in yet, but since the parser
+ // will consume the buffer we need to be able to backtrack.
+ dataBuffer.mark();
+
+ IncomingMessage message = messageParser.parseMessage(dataBuffer);
+
+ if (message == null) {
+ // Nothing to do. Reset the buffer and exit the loop.
+ dataBuffer.reset();
+ break;
+ }
+
+ if (message instanceof IncomingRequestMessage) {
+ // Received a request. Give it to the request handler
+ if (requestHandler != null) {
+ OutgoingResponseMessage response = requestHandler
+ .handleRequest((IncomingRequestMessage) message);
+
+ if (response != null)
+ send(response);
+ }
+ } else
+ // Must be a response. Give it to the waiting room.
+ waitingRoom.response((IncomingResponseMessage) message);
+ } catch (Exception e) {
+ exceptionHandler.receivedException(e);
+ // Clear the buffer
+ // dataBuffer.clear();
+ }
+ }
+ }
+
+ private void write(byte[] data) throws IOException {
+ if (ioLog != null)
+ ioLog.output(data);
+
+ synchronized (transport) {
+ transport.write(data);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void handleIOException(IOException e) {
+ exceptionHandler.receivedException(e);
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/MessageParser.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/MessageParser.java
new file mode 100644
index 0000000..b489b15
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/MessageParser.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue.ByteQueue;
+
+/**
+ * Interface defining methods that are called when data arrives in the connection.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public interface MessageParser {
+ /**
+ * Attempt to parse a message out of the queue. Data in the queue may be discarded if it is unusable (i.e. a start
+ * indicator is not found), but otherwise if a message is not found due to the data being incomplete, the method
+ * should return null. As additional data arrives, it will be appended to the queue and this method will be called
+ * again.
+ *
+ * Implementations should not modify the queue unless it is safe to do so. No copy of the data is made before
+ * calling this method.
+ *
+ * @param queue the queue from which to access data for the creation of the message
+ * @return the message if one was able to be created, or null otherwise.
+ * @throws Exception if the data in the queue is sufficient to construct a message, but the message data is invalid, this
+ * method must throw an exception, or it will keep getting the same data.
+ */
+ IncomingMessage parseMessage(ByteQueue queue) throws Exception;
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/MessagingExceptionHandler.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/MessagingExceptionHandler.java
new file mode 100644
index 0000000..fc60c9b
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/MessagingExceptionHandler.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging;
+
+/**
+ *
MessagingExceptionHandler interface.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public interface MessagingExceptionHandler {
+ /**
+ * receivedException.
+ *
+ * @param e a {@link Exception} object.
+ */
+ public void receivedException(Exception e);
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/OutgoingMessage.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/OutgoingMessage.java
new file mode 100644
index 0000000..b43823c
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/OutgoingMessage.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging;
+
+/**
+ * OutgoingMessage interface.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public interface OutgoingMessage {
+ /**
+ * Return the byte array representing the serialization of the request.
+ *
+ * @return byte array representing the serialization of the request
+ */
+ byte[] getMessageData();
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/OutgoingRequestMessage.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/OutgoingRequestMessage.java
new file mode 100644
index 0000000..4f7552f
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/OutgoingRequestMessage.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging;
+
+
+/**
+ * OutgoingRequestMessage interface.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public interface OutgoingRequestMessage extends OutgoingMessage {
+ /**
+ * Whether the request is expecting a response or not.
+ *
+ * @return true if a response is expected, false otherwise.
+ */
+ boolean expectsResponse();
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/OutgoingResponseMessage.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/OutgoingResponseMessage.java
new file mode 100644
index 0000000..2375b0a
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/OutgoingResponseMessage.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging;
+
+
+/**
+ * OutgoingResponseMessage interface.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public interface OutgoingResponseMessage extends OutgoingMessage {
+ // A marker interface.
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/RequestHandler.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/RequestHandler.java
new file mode 100644
index 0000000..6bbda5e
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/RequestHandler.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging;
+
+
+/**
+ * RequestHandler interface.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public interface RequestHandler {
+ /**
+ * Handle the request and return the appropriate response object.
+ *
+ * @param request the request to handle
+ * @return the response object or null if no response is to be sent. null may also be returned if the request is
+ * handled asynchronously.
+ * @throws Exception if necessary
+ */
+ OutgoingResponseMessage handleRequest(IncomingRequestMessage request) throws Exception;
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/StreamTransport.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/StreamTransport.java
new file mode 100644
index 0000000..bdc015e
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/StreamTransport.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * First, instatiate with the streams. Then add a data consumer, or create a message control and pass this as the
+ * transport (which will make the message control the data consumer). Change the read delay if desired. This class
+ * supports running in its own thread (start) or an external one (run), say from a thread pool. Both approaches are
+ * delegated to the stream listener. In either case, stop the transport with the stop method (or just stop the message
+ * control).
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class StreamTransport implements Transport, Runnable {
+ protected OutputStream out;
+ protected InputStream in;
+ private InputStreamListener listener;
+
+ /**
+ * Constructor for StreamTransport.
+ *
+ * @param in a {@link InputStream} object.
+ * @param out a {@link OutputStream} object.
+ */
+ public StreamTransport(InputStream in, OutputStream out) {
+ this.out = out;
+ this.in = in;
+ }
+
+ /**
+ * setReadDelay.
+ *
+ * @param readDelay a int.
+ */
+ public void setReadDelay(int readDelay) {
+ if (listener != null)
+ listener.setReadDelay(readDelay);
+ }
+
+ /**
+ * start.
+ *
+ * @param threadName a {@link String} object.
+ */
+ public void start(String threadName) {
+ listener.start(threadName);
+ }
+
+ /**
+ * stop.
+ */
+ public void stop() {
+ listener.stop();
+ }
+
+ /**
+ * run.
+ */
+ public void run() {
+ listener.run();
+ }
+
+
+ public void setConsumer(DataConsumer consumer) {
+ listener = new InputStreamListener(in, consumer);
+ }
+
+ /**
+ * removeConsumer.
+ */
+ public void removeConsumer() {
+ listener.stop();
+ listener = null;
+ }
+
+ /**
+ * write.
+ *
+ * @param data an array of {@link byte} objects.
+ * @throws IOException if any.
+ */
+ public void write(byte[] data) throws IOException {
+ out.write(data);
+ out.flush();
+ }
+
+
+ public void write(byte[] data, int len) throws IOException {
+ out.write(data, 0, len);
+ out.flush();
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/StreamTransportCharSpaced.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/StreamTransportCharSpaced.java
new file mode 100644
index 0000000..5a17c72
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/StreamTransportCharSpaced.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * StreamTransportCharSpaced class.
+ *
+ * @author Terry Packer
+ * @version 2025.9.0
+ */
+public class StreamTransportCharSpaced extends StreamTransport {
+
+ private final long charSpacing;
+
+ /**
+ * Constructor for StreamTransportCharSpaced.
+ *
+ * @param in a {@link InputStream} object.
+ * @param out a {@link OutputStream} object.
+ * @param charSpacing a long.
+ */
+ public StreamTransportCharSpaced(InputStream in, OutputStream out, long charSpacing) {
+ super(in, out);
+ this.charSpacing = charSpacing;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Perform a write, ensure space between chars
+ */
+ @Override
+ public void write(byte[] data) throws IOException {
+
+ try {
+ long waited = 0, writeStart, writeEnd, waitRemaining;
+ for (byte b : data) {
+ writeStart = System.nanoTime();
+ out.write(b);
+ writeEnd = System.nanoTime();
+ waited = writeEnd - writeStart;
+ if (waited < this.charSpacing) {
+ waitRemaining = this.charSpacing - waited;
+ Thread.sleep(waitRemaining / 1000000, (int) (waitRemaining % 1000000));
+ }
+
+ }
+ } catch (Exception e) {
+ throw new IOException(e);
+ }
+ out.flush();
+ }
+
+
+ public void write(byte[] data, int len) throws IOException {
+ try {
+ long waited = 0, writeStart, writeEnd, waitRemaining;
+ for (int i = 0; i < len; i++) {
+ writeStart = System.nanoTime();
+ out.write(data[i]);
+ writeEnd = System.nanoTime();
+ waited = writeEnd - writeStart;
+ if (waited < this.charSpacing) {
+ waitRemaining = this.charSpacing - waited;
+ Thread.sleep(waitRemaining / 1000000, (int) (waitRemaining % 1000000));
+ }
+
+ }
+ } catch (Exception e) {
+ throw new IOException(e);
+ }
+ out.flush();
+ }
+
+
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/TestableTransport.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/TestableTransport.java
new file mode 100644
index 0000000..0834be5
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/TestableTransport.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Provides synchronization on the input stream read by wrapping it.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class TestableTransport extends StreamTransport {
+ /**
+ *
Constructor for TestableTransport.
+ *
+ * @param in a {@link InputStream} object.
+ * @param out a {@link OutputStream} object.
+ */
+ public TestableTransport(InputStream in, OutputStream out) {
+ super(new TestableBufferedInputStream(in), out);
+ }
+
+ /**
+ * testInputStream.
+ *
+ * @throws IOException if any.
+ */
+ public void testInputStream() throws IOException {
+ ((TestableBufferedInputStream) in).test();
+ }
+
+ static class TestableBufferedInputStream extends BufferedInputStream {
+ public TestableBufferedInputStream(InputStream in) {
+ super(in);
+ }
+
+ @Override
+ public synchronized int read(byte[] buf) throws IOException {
+ return super.read(buf);
+ }
+
+ public synchronized void test() throws IOException {
+ mark(1);
+ int i = read();
+ if (i == -1)
+ throw new IOException("Stream closed");
+ reset();
+ }
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/TimeoutException.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/TimeoutException.java
new file mode 100644
index 0000000..3c014eb
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/TimeoutException.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging;
+
+import java.io.IOException;
+import java.io.Serial;
+
+/**
+ * TimeoutException class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class TimeoutException extends IOException {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Constructor for TimeoutException.
+ *
+ * @param message a {@link String} object.
+ */
+ public TimeoutException(String message) {
+ super(message);
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/Transport.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/Transport.java
new file mode 100644
index 0000000..95a9b04
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/Transport.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging;
+
+import java.io.IOException;
+
+/**
+ * A transport is a wrapper around the means by which data is transferred. So, there could be transports for serial
+ * ports, sockets, UDP, email, etc.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public interface Transport {
+ /**
+ * setConsumer.
+ *
+ * @param consumer a {@link DataConsumer} object.
+ * @throws IOException if any.
+ */
+ abstract void setConsumer(DataConsumer consumer) throws IOException;
+
+ /**
+ * removeConsumer.
+ */
+ abstract void removeConsumer();
+
+ /**
+ * write.
+ *
+ * @param data an array of {@link byte} objects.
+ * @throws IOException if any.
+ */
+ abstract void write(byte[] data) throws IOException;
+
+ /**
+ * write.
+ *
+ * @param data an array of {@link byte} objects.
+ * @param len a int.
+ * @throws IOException if any.
+ */
+ abstract void write(byte[] data, int len) throws IOException;
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/WaitingRoom.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/WaitingRoom.java
new file mode 100644
index 0000000..af20eb3
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/WaitingRoom.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * The waiting room is a place for request messages to hang out while awaiting their responses.
+ *
+ * @author Matthew Lohbihler
+ */
+class WaitingRoom {
+ private static final Log LOG = LogFactory.getLog(WaitingRoom.class);
+
+ private final Map waitHere = new HashMap();
+
+ private WaitingRoomKeyFactory keyFactory;
+
+ void setKeyFactory(WaitingRoomKeyFactory keyFactory) {
+ this.keyFactory = keyFactory;
+ }
+
+ /**
+ * The request message should be sent AFTER entering the waiting room so that the (vanishingly small) chance of a
+ * response being returned before the thread is waiting for it is eliminated.
+ *
+ * @return
+ */
+ void enter(WaitingRoomKey key) {
+ Member member = new Member();
+ synchronized (this) {
+ while (waitHere.get(key) != null) {
+ if (LOG.isDebugEnabled())
+ LOG.debug("Duplicate waiting room key found. Waiting for member to leave.");
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ // no op
+ }
+ }
+ // Member dup = waitHere.get(key);
+ // if (dup != null) {
+ //
+ // throw new WaitingRoomException("Waiting room too crowded. Already contains the key " + key);
+ // }
+
+ waitHere.put(key, member);
+ }
+ }
+
+ IncomingResponseMessage getResponse(WaitingRoomKey key, long timeout) throws WaitingRoomException {
+ // Get the member.
+ Member member;
+ synchronized (this) {
+ member = waitHere.get(key);
+ }
+
+ if (member == null)
+ throw new WaitingRoomException("No member for key " + key);
+
+ // Wait for the response.
+ return member.getResponse(timeout);
+ }
+
+ void leave(WaitingRoomKey key) {
+ // Leave the waiting room
+ synchronized (this) {
+ waitHere.remove(key);
+
+ // Notify any threads that are waiting to get in. This could probably be just a notify() call.
+ notifyAll();
+ }
+ }
+
+ /**
+ * This method is used by the data listening thread to post responses as they are received from the transport.
+ *
+ * @param response the response message
+ * @throws WaitingRoomException
+ */
+ void response(IncomingResponseMessage response) throws WaitingRoomException {
+ WaitingRoomKey key = keyFactory.createWaitingRoomKey(response);
+ if (key == null)
+ // The key factory can return a null key if the response should be ignored.
+ return;
+
+ Member member;
+
+ synchronized (this) {
+ member = waitHere.get(key);
+ }
+
+ if (member != null)
+ member.setResponse(response);
+ else
+ throw new WaitingRoomException("No recipient was found waiting for response for key " + key);
+ }
+
+ /**
+ * This class is used by network message controllers to manage the blocking of threads sending confirmed messages.
+ * The instance itself serves as a monitor upon which the sending thread can wait (with a timeout). When a response
+ * is received, the message controller can set it in here, automatically notifying the sending thread that the
+ * response is available.
+ *
+ * @author Matthew Lohbihler
+ */
+ class Member {
+ private IncomingResponseMessage response;
+
+ synchronized void setResponse(IncomingResponseMessage response) {
+ this.response = response;
+ notify();
+ }
+
+ synchronized IncomingResponseMessage getResponse(long timeout) {
+ // Check if there is a response object now.
+ if (response != null)
+ return response;
+
+ // If not, wait the timeout and then check again.
+ waitNoThrow(timeout);
+ return response;
+ }
+
+ private void waitNoThrow(long timeout) {
+ try {
+ wait(timeout);
+ } catch (InterruptedException e) {
+ // Ignore
+ }
+ }
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/WaitingRoomException.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/WaitingRoomException.java
new file mode 100644
index 0000000..3cc38c6
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/WaitingRoomException.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging;
+
+import java.io.IOException;
+import java.io.Serial;
+
+/**
+ * WaitingRoomException class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class WaitingRoomException extends IOException {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Constructor for WaitingRoomException.
+ *
+ * @param message a {@link String} object.
+ */
+ public WaitingRoomException(String message) {
+ super(message);
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/WaitingRoomKey.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/WaitingRoomKey.java
new file mode 100644
index 0000000..33f21cd
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/WaitingRoomKey.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging;
+
+/**
+ * Waiting room keys are used to match requests with responses. Implementation need to have hashcode and equals
+ * definitions.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public interface WaitingRoomKey {
+ // Implementation needs to have hashcode and equals implementations.
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/WaitingRoomKeyFactory.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/WaitingRoomKeyFactory.java
new file mode 100644
index 0000000..f537f4b
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/messaging/WaitingRoomKeyFactory.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.messaging;
+
+
+/**
+ * WaitingRoomKeyFactory interface.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public interface WaitingRoomKeyFactory {
+ /**
+ * createWaitingRoomKey.
+ *
+ * @param request a {@link OutgoingRequestMessage} object.
+ * @return a {@link WaitingRoomKey} object.
+ */
+ WaitingRoomKey createWaitingRoomKey(OutgoingRequestMessage request);
+
+ /**
+ * createWaitingRoomKey.
+ *
+ * @param response a {@link IncomingResponseMessage} object.
+ * @return a {@link WaitingRoomKey} object.
+ */
+ WaitingRoomKey createWaitingRoomKey(IncomingResponseMessage response);
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/timer/SystemTimeSource.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/timer/SystemTimeSource.java
new file mode 100644
index 0000000..36179a1
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/timer/SystemTimeSource.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.timer;
+
+/**
+ * An implementation of TimeSource that returns the host time via System.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class SystemTimeSource implements TimeSource {
+ /**
+ * currentTimeMillis.
+ *
+ * @return a long.
+ */
+ public long currentTimeMillis() {
+ return System.currentTimeMillis();
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/timer/TimeSource.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/timer/TimeSource.java
new file mode 100644
index 0000000..ad0e92f
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/timer/TimeSource.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.timer;
+
+/**
+ * An interface to abstract the source of current time away from System. This allows code to run in simulations where
+ * the time is controlled explicitly.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public interface TimeSource {
+ /**
+ * currentTimeMillis.
+ *
+ * @return a long.
+ */
+ long currentTimeMillis();
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/util/ArrayUtils.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/util/ArrayUtils.java
new file mode 100644
index 0000000..d83cb6d
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/util/ArrayUtils.java
@@ -0,0 +1,373 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util;
+
+import java.util.List;
+
+/**
+ * ArrayUtils class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class ArrayUtils {
+ private static int[] bitFromMask = {0xff, 0x7f, 0x3f, 0x1f, 0xf, 0x7, 0x3, 0x1};
+
+ /**
+ * toHexString.
+ *
+ * @param bytes an array of {@link byte} objects.
+ * @return a {@link String} object.
+ */
+ public static String toHexString(byte[] bytes) {
+ return toHexString(bytes, 0, bytes.length);
+ }
+
+ /**
+ * toHexString.
+ *
+ * @param bytes an array of {@link byte} objects.
+ * @param start a int.
+ * @param len a int.
+ * @return a {@link String} object.
+ */
+ public static String toHexString(byte[] bytes, int start, int len) {
+ if (len == 0)
+ return "[]";
+
+ StringBuffer sb = new StringBuffer();
+ sb.append('[');
+ sb.append(Integer.toHexString(bytes[start] & 0xff));
+ for (int i = 1; i < len; i++)
+ sb.append(',').append(Integer.toHexString(bytes[start + i] & 0xff));
+ sb.append("]");
+
+ return sb.toString();
+ }
+
+ /**
+ * toPlainHexString.
+ *
+ * @param bytes an array of {@link byte} objects.
+ * @return a {@link String} object.
+ */
+ public static String toPlainHexString(byte[] bytes) {
+ return toPlainHexString(bytes, 0, bytes.length);
+ }
+
+ /**
+ * toPlainHexString.
+ *
+ * @param bytes an array of {@link byte} objects.
+ * @param start a int.
+ * @param len a int.
+ * @return a {@link String} object.
+ */
+ public static String toPlainHexString(byte[] bytes, int start, int len) {
+ StringBuffer sb = new StringBuffer();
+ for (int i = 0; i < len; i++) {
+ String s = Integer.toHexString(bytes[start + i] & 0xff);
+ if (s.length() < 2)
+ sb.append('0');
+ sb.append(s);
+ }
+ return sb.toString();
+ }
+
+ /**
+ * toString.
+ *
+ * @param bytes an array of {@link byte} objects.
+ * @return a {@link String} object.
+ */
+ public static String toString(byte[] bytes) {
+ return toString(bytes, 0, bytes.length);
+ }
+
+ /**
+ * toString.
+ *
+ * @param bytes an array of {@link byte} objects.
+ * @param start a int.
+ * @param len a int.
+ * @return a {@link String} object.
+ */
+ public static String toString(byte[] bytes, int start, int len) {
+ if (len == 0)
+ return "[]";
+
+ StringBuffer sb = new StringBuffer();
+ sb.append('[');
+ sb.append(Integer.toString(bytes[start] & 0xff));
+ for (int i = 1; i < len; i++)
+ sb.append(',').append(Integer.toString(bytes[start + i] & 0xff));
+ sb.append("]");
+
+ return sb.toString();
+ }
+
+ /**
+ * isEmpty.
+ *
+ * @param value an array of {@link int} objects.
+ * @return a boolean.
+ */
+ public static boolean isEmpty(int[] value) {
+ return value == null || value.length == 0;
+ }
+
+ /**
+ * indexOf.
+ *
+ * @param values an array of {@link String} objects.
+ * @param value a {@link String} object.
+ * @return a int.
+ */
+ public static int indexOf(String[] values, String value) {
+ if (values == null)
+ return -1;
+
+ for (int i = 0; i < values.length; i++) {
+ if (values[i].equals(value))
+ return i;
+ }
+
+ return -1;
+ }
+
+ /**
+ * containsIgnoreCase.
+ *
+ * @param values an array of {@link String} objects.
+ * @param value a {@link String} object.
+ * @return a boolean.
+ */
+ public static boolean containsIgnoreCase(String[] values, String value) {
+ if (values == null)
+ return false;
+
+ for (int i = 0; i < values.length; i++) {
+ if (values[i].equalsIgnoreCase(value))
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * indexOf.
+ *
+ * @param src an array of {@link byte} objects.
+ * @param target an array of {@link byte} objects.
+ * @return a int.
+ */
+ public static int indexOf(byte[] src, byte[] target) {
+ return indexOf(src, 0, src.length, target);
+ }
+
+ /**
+ * indexOf.
+ *
+ * @param src an array of {@link byte} objects.
+ * @param len a int.
+ * @param target an array of {@link byte} objects.
+ * @return a int.
+ */
+ public static int indexOf(byte[] src, int len, byte[] target) {
+ return indexOf(src, 0, len, target);
+ }
+
+ /**
+ * indexOf.
+ *
+ * @param src an array of {@link byte} objects.
+ * @param start a int.
+ * @param len a int.
+ * @param target an array of {@link byte} objects.
+ * @return a int.
+ */
+ public static int indexOf(byte[] src, int start, int len, byte[] target) {
+ int pos = start;
+ int i;
+ boolean matched;
+ while (pos + target.length <= len) {
+ // Check for a match on the first character
+ if (src[pos] == target[0]) {
+ // Now check for matches in the rest of the characters
+ matched = true;
+ i = 1;
+ while (i < target.length) {
+ if (src[pos + i] != target[i]) {
+ matched = false;
+ break;
+ }
+ i++;
+ }
+
+ if (matched)
+ return pos;
+ }
+ pos++;
+ }
+
+ return -1;
+ }
+
+ /**
+ * Returns the value of the bits in the given range. Ranges can extend multiple bytes. No range checking is done.
+ * Invalid ranges will result in {@link ArrayIndexOutOfBoundsException}.
+ *
+ * @param b the array of bytes.
+ * @param offset the location at which to begin
+ * @param length the number of bits to include in the value.
+ * @return the value of the bits in the range.
+ */
+ public static long bitRangeValueLong(byte[] b, int offset, int length) {
+ if (length <= 0)
+ return 0;
+
+ int byteFrom = offset / 8;
+ int byteTo = (offset + length - 1) / 8;
+
+ long result = b[byteFrom] & bitFromMask[offset % 8];
+
+ for (int i = byteFrom + 1; i <= byteTo; i++) {
+ result <<= 8;
+ result |= b[i] & 0xff;
+ }
+
+ result >>= 8 - (((offset + length - 1) % 8) + 1);
+
+ return result;
+ }
+
+ /**
+ * bitRangeValue.
+ *
+ * @param b an array of {@link byte} objects.
+ * @param offset a int.
+ * @param length a int.
+ * @return a int.
+ */
+ public static int bitRangeValue(byte[] b, int offset, int length) {
+ return (int) bitRangeValueLong(b, offset, length);
+ }
+
+ /**
+ * byteRangeValueLong.
+ *
+ * @param b an array of {@link byte} objects.
+ * @param offset a int.
+ * @param length a int.
+ * @return a long.
+ */
+ public static long byteRangeValueLong(byte[] b, int offset, int length) {
+ long result = 0;
+
+ for (int i = offset; i < offset + length; i++) {
+ result <<= 8;
+ result |= b[i] & 0xff;
+ }
+
+ return result;
+ }
+
+ /**
+ * byteRangeValue.
+ *
+ * @param b an array of {@link byte} objects.
+ * @param offset a int.
+ * @param length a int.
+ * @return a int.
+ */
+ public static int byteRangeValue(byte[] b, int offset, int length) {
+ return (int) byteRangeValueLong(b, offset, length);
+ }
+
+ /**
+ * sum.
+ *
+ * @param a an array of {@link int} objects.
+ * @return a int.
+ */
+ public static int sum(int[] a) {
+ int sum = 0;
+ for (int i = 0; i < a.length; i++)
+ sum += a[i];
+ return sum;
+ }
+
+ /**
+ * toIntArray.
+ *
+ * @param list a {@link List} object.
+ * @return an array of {@link int} objects.
+ */
+ public static int[] toIntArray(List list) {
+ int[] result = new int[list.size()];
+ for (int i = 0; i < result.length; i++)
+ result[i] = list.get(i);
+ return result;
+ }
+
+ /**
+ * toDoubleArray.
+ *
+ * @param list a {@link List} object.
+ * @return an array of {@link double} objects.
+ */
+ public static double[] toDoubleArray(List list) {
+ double[] result = new double[list.size()];
+ for (int i = 0; i < result.length; i++)
+ result[i] = list.get(i);
+ return result;
+ }
+
+ /**
+ * concatenate.
+ *
+ * @param a an array of {@link Object} objects.
+ * @param delimiter a {@link String} object.
+ * @return a {@link String} object.
+ */
+ public static String concatenate(Object[] a, String delimiter) {
+ StringBuilder sb = new StringBuilder();
+ boolean first = true;
+ for (Object o : a) {
+ if (first)
+ first = false;
+ else
+ sb.append(delimiter);
+ sb.append(o);
+ }
+ return sb.toString();
+ }
+
+ /**
+ * shift.
+ *
+ * @param a an array of {@link Object} objects.
+ * @param count a int.
+ */
+ public static void shift(Object[] a, int count) {
+ if (count > 0)
+ System.arraycopy(a, 0, a, count, a.length - count);
+ else
+ System.arraycopy(a, -count, a, 0, a.length + count);
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/util/ProgressiveTask.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/util/ProgressiveTask.java
new file mode 100644
index 0000000..46c7084
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/util/ProgressiveTask.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util;
+
+
+/**
+ * Abstract ProgressiveTask class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+abstract public class ProgressiveTask implements Runnable {
+ protected boolean completed = false;
+ private boolean cancelled = false;
+ private ProgressiveTaskListener listener;
+
+ /**
+ * Constructor for ProgressiveTask.
+ */
+ public ProgressiveTask() {
+ // no op
+ }
+
+ /**
+ * Constructor for ProgressiveTask.
+ *
+ * @param l a {@link ProgressiveTaskListener} object.
+ */
+ public ProgressiveTask(ProgressiveTaskListener l) {
+ listener = l;
+ }
+
+ /**
+ * cancel.
+ */
+ public void cancel() {
+ cancelled = true;
+ }
+
+ /**
+ * isCancelled.
+ *
+ * @return a boolean.
+ */
+ public boolean isCancelled() {
+ return cancelled;
+ }
+
+ /**
+ * isCompleted.
+ *
+ * @return a boolean.
+ */
+ public boolean isCompleted() {
+ return completed;
+ }
+
+ /**
+ * run.
+ */
+ public final void run() {
+ while (true) {
+ if (isCancelled()) {
+ declareFinished(true);
+ break;
+ }
+
+ runImpl();
+
+ if (isCompleted()) {
+ declareFinished(false);
+ break;
+ }
+ }
+ completed = true;
+ }
+
+ /**
+ * declareProgress.
+ *
+ * @param progress a float.
+ */
+ protected void declareProgress(float progress) {
+ ProgressiveTaskListener l = listener;
+ if (l != null)
+ l.progressUpdate(progress);
+ }
+
+ private void declareFinished(boolean cancelled) {
+ ProgressiveTaskListener l = listener;
+ if (l != null) {
+ if (cancelled)
+ l.taskCancelled();
+ else
+ l.taskCompleted();
+ }
+ }
+
+ /**
+ * Implementers of this method MUST return from it occasionally so that the cancelled status can be checked. Each
+ * return must leave the class and thread state with the expectation that runImpl will not be called again, while
+ * acknowledging the possibility that it will.
+ *
+ * Implementations SHOULD call the declareProgress method with each runImpl execution such that the listener can be
+ * notified.
+ *
+ * Implementations MUST set the completed field to true when the task is finished.
+ */
+ abstract protected void runImpl();
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/util/ProgressiveTaskListener.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/util/ProgressiveTaskListener.java
new file mode 100644
index 0000000..5552a3d
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/util/ProgressiveTaskListener.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util;
+
+/**
+ *
ProgressiveTaskListener interface.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public interface ProgressiveTaskListener {
+ /**
+ * Optionally called occasionally by the task to declare the progress that has been made.
+ *
+ * @param progress float between 0 and 1 where 0 is no progress and 1 is completed.
+ */
+ void progressUpdate(float progress);
+
+ /**
+ * Notification that the task has been cancelled. Should only be called once for the task.
+ */
+ void taskCancelled();
+
+ /**
+ * Notification that the task has been completed. Should only be called once for the task.
+ */
+ void taskCompleted();
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/util/queue/ByteQueue.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/util/queue/ByteQueue.java
new file mode 100644
index 0000000..8413967
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/sero/util/queue/ByteQueue.java
@@ -0,0 +1,862 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.sero.util.queue;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+
+/**
+ * ByteQueue class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+public class ByteQueue implements Cloneable {
+ private byte[] queue;
+ private int head = -1;
+ private int tail = 0;
+ private int size = 0;
+
+ private int markHead;
+ private int markTail;
+ private int markSize;
+
+ /**
+ * Constructor for ByteQueue.
+ */
+ public ByteQueue() {
+ this(1024);
+ }
+
+ /**
+ * Constructor for ByteQueue.
+ *
+ * @param initialLength a int.
+ */
+ public ByteQueue(int initialLength) {
+ queue = new byte[initialLength];
+ }
+
+ /**
+ * Constructor for ByteQueue.
+ *
+ * @param b an array of {@link byte} objects.
+ */
+ public ByteQueue(byte[] b) {
+ this(b.length);
+ push(b, 0, b.length);
+ }
+
+ /**
+ * Constructor for ByteQueue.
+ *
+ * @param b an array of {@link byte} objects.
+ * @param pos a int.
+ * @param length a int.
+ */
+ public ByteQueue(byte[] b, int pos, int length) {
+ this(length);
+ push(b, pos, length);
+ }
+
+ /**
+ * Constructor for ByteQueue.
+ *
+ * @param hex a {@link String} object.
+ */
+ public ByteQueue(String hex) {
+ this(hex.length() / 2);
+ push(hex);
+ }
+
+ /**
+ * push.
+ *
+ * @param hex a {@link String} object.
+ */
+ public void push(String hex) {
+ if (hex.length() % 2 != 0)
+ throw new IllegalArgumentException("Hex string must have an even number of characters");
+ byte[] b = new byte[hex.length() / 2];
+ for (int i = 0; i < b.length; i++)
+ b[i] = (byte) Integer.parseInt(hex.substring(i * 2, i * 2 + 2), 16);
+ push(b, 0, b.length);
+ }
+
+ /**
+ * push.
+ *
+ * @param b a byte.
+ */
+ public void push(byte b) {
+ if (room() == 0)
+ expand();
+
+ queue[tail] = b;
+
+ if (head == -1)
+ head = 0;
+ tail = (tail + 1) % queue.length;
+ size++;
+ }
+
+ /**
+ * push.
+ *
+ * @param i a int.
+ */
+ public void push(int i) {
+ push((byte) i);
+ }
+
+ /**
+ * push.
+ *
+ * @param l a long.
+ */
+ public void push(long l) {
+ push((byte) l);
+ }
+
+ /**
+ * Push unsigned 2 bytes.
+ *
+ * @param i a int.
+ */
+ public void pushU2B(int i) {
+ push((byte) (i >> 8));
+ push((byte) i);
+ }
+
+ /**
+ * Push unsigned 3 bytes.
+ *
+ * @param i a int.
+ */
+ public void pushU3B(int i) {
+ push((byte) (i >> 16));
+ push((byte) (i >> 8));
+ push((byte) i);
+ }
+
+ /**
+ * Push signed 4 bytes.
+ *
+ * @param i a int.
+ */
+ public void pushS4B(int i) {
+ pushInt(i);
+ }
+
+ /**
+ * Push unsigned 4 bytes.
+ *
+ * @param l a long.
+ */
+ public void pushU4B(long l) {
+ push((byte) (l >> 24));
+ push((byte) (l >> 16));
+ push((byte) (l >> 8));
+ push((byte) l);
+ }
+
+ /**
+ * pushChar.
+ *
+ * @param c a char.
+ */
+ public void pushChar(char c) {
+ push((byte) (c >> 8));
+ push((byte) c);
+ }
+
+ /**
+ * pushDouble.
+ *
+ * @param d a double.
+ */
+ public void pushDouble(double d) {
+ pushLong(Double.doubleToLongBits(d));
+ }
+
+ /**
+ * pushFloat.
+ *
+ * @param f a float.
+ */
+ public void pushFloat(float f) {
+ pushInt(Float.floatToIntBits(f));
+ }
+
+ /**
+ * pushInt.
+ *
+ * @param i a int.
+ */
+ public void pushInt(int i) {
+ push((byte) (i >> 24));
+ push((byte) (i >> 16));
+ push((byte) (i >> 8));
+ push((byte) i);
+ }
+
+ /**
+ * pushLong.
+ *
+ * @param l a long.
+ */
+ public void pushLong(long l) {
+ push((byte) (l >> 56));
+ push((byte) (l >> 48));
+ push((byte) (l >> 40));
+ push((byte) (l >> 32));
+ push((byte) (l >> 24));
+ push((byte) (l >> 16));
+ push((byte) (l >> 8));
+ push((byte) l);
+ }
+
+ /**
+ * pushShort.
+ *
+ * @param s a short.
+ */
+ public void pushShort(short s) {
+ push((byte) (s >> 8));
+ push((byte) s);
+ }
+
+ /**
+ * read.
+ *
+ * @param in a {@link InputStream} object.
+ * @param length a int.
+ * @throws IOException if any.
+ */
+ public void read(InputStream in, int length) throws IOException {
+ if (length == 0)
+ return;
+
+ while (room() < length)
+ expand();
+
+ int tailLength = queue.length - tail;
+ if (tailLength > length)
+ readImpl(in, tail, length);
+ else
+ readImpl(in, tail, tailLength);
+
+ if (length > tailLength)
+ readImpl(in, 0, length - tailLength);
+
+ if (head == -1)
+ head = 0;
+ tail = (tail + length) % queue.length;
+ size += length;
+ }
+
+ private void readImpl(InputStream in, int offset, int length) throws IOException {
+ int readcount;
+ while (length > 0) {
+ readcount = in.read(queue, offset, length);
+ offset += readcount;
+ length -= readcount;
+ }
+ }
+
+ /**
+ * push.
+ *
+ * @param b an array of {@link byte} objects.
+ */
+ public void push(byte[] b) {
+ push(b, 0, b.length);
+ }
+
+ /**
+ * push.
+ *
+ * @param b an array of {@link byte} objects.
+ * @param pos a int.
+ * @param length a int.
+ */
+ public void push(byte[] b, int pos, int length) {
+ if (length == 0)
+ return;
+
+ while (room() < length)
+ expand();
+
+ int tailLength = queue.length - tail;
+ if (tailLength > length)
+ System.arraycopy(b, pos, queue, tail, length);
+ else
+ System.arraycopy(b, pos, queue, tail, tailLength);
+
+ if (length > tailLength)
+ System.arraycopy(b, tailLength + pos, queue, 0, length - tailLength);
+
+ if (head == -1)
+ head = 0;
+ tail = (tail + length) % queue.length;
+ size += length;
+ }
+
+ /**
+ * push.
+ *
+ * @param source a {@link ByteQueue} object.
+ */
+ public void push(ByteQueue source) {
+ if (source.size == 0)
+ return;
+
+ if (source == this)
+ source = (ByteQueue) clone();
+
+ int firstCopyLen = source.queue.length - source.head;
+ if (source.size < firstCopyLen)
+ firstCopyLen = source.size;
+ push(source.queue, source.head, firstCopyLen);
+
+ if (firstCopyLen < source.size)
+ push(source.queue, 0, source.tail);
+ }
+
+ /**
+ * push.
+ *
+ * @param source a {@link ByteQueue} object.
+ * @param len a int.
+ */
+ public void push(ByteQueue source, int len) {
+ // TODO There is certainly a more elegant way to do this...
+ while (len-- > 0)
+ push(source.pop());
+ }
+
+ /**
+ * push.
+ *
+ * @param source a {@link ByteBuffer} object.
+ */
+ public void push(ByteBuffer source) {
+ int length = source.remaining();
+ if (length == 0)
+ return;
+
+ while (room() < length)
+ expand();
+
+ int tailLength = queue.length - tail;
+ if (tailLength > length)
+ source.get(queue, tail, length);
+ else
+ source.get(queue, tail, tailLength);
+
+ if (length > tailLength)
+ source.get(queue, 0, length - tailLength);
+
+ if (head == -1)
+ head = 0;
+ tail = (tail + length) % queue.length;
+ size += length;
+ }
+
+ /**
+ * mark.
+ */
+ public void mark() {
+ markHead = head;
+ markTail = tail;
+ markSize = size;
+ }
+
+ /**
+ * reset.
+ */
+ public void reset() {
+ head = markHead;
+ tail = markTail;
+ size = markSize;
+ }
+
+ /**
+ * pop.
+ *
+ * @return a byte.
+ */
+ public byte pop() {
+ byte retval = queue[head];
+
+ if (size == 1) {
+ head = -1;
+ tail = 0;
+ } else
+ head = (head + 1) % queue.length;
+
+ size--;
+
+ return retval;
+ }
+
+ /**
+ * popU1B.
+ *
+ * @return a int.
+ */
+ public int popU1B() {
+ return pop() & 0xff;
+ }
+
+ /**
+ * popU2B.
+ *
+ * @return a int.
+ */
+ public int popU2B() {
+ return ((pop() & 0xff) << 8) | (pop() & 0xff);
+ }
+
+ /**
+ * popU3B.
+ *
+ * @return a int.
+ */
+ public int popU3B() {
+ return ((pop() & 0xff) << 16) | ((pop() & 0xff) << 8) | (pop() & 0xff);
+ }
+
+ /**
+ * popS2B.
+ *
+ * @return a short.
+ */
+ public short popS2B() {
+ return (short) (((pop() & 0xff) << 8) | (pop() & 0xff));
+ }
+
+ /**
+ * popS4B.
+ *
+ * @return a int.
+ */
+ public int popS4B() {
+ return ((pop() & 0xff) << 24) | ((pop() & 0xff) << 16) | ((pop() & 0xff) << 8) | (pop() & 0xff);
+ }
+
+ /**
+ * popU4B.
+ *
+ * @return a long.
+ */
+ public long popU4B() {
+ return ((long) (pop() & 0xff) << 24) | ((long) (pop() & 0xff) << 16) | ((long) (pop() & 0xff) << 8)
+ | (pop() & 0xff);
+ }
+
+ /**
+ * pop.
+ *
+ * @param buf an array of {@link byte} objects.
+ * @return a int.
+ */
+ public int pop(byte[] buf) {
+ return pop(buf, 0, buf.length);
+ }
+
+ /**
+ * pop.
+ *
+ * @param buf an array of {@link byte} objects.
+ * @param pos a int.
+ * @param length a int.
+ * @return a int.
+ */
+ public int pop(byte[] buf, int pos, int length) {
+ length = peek(buf, pos, length);
+
+ size -= length;
+
+ if (size == 0) {
+ head = -1;
+ tail = 0;
+ } else
+ head = (head + length) % queue.length;
+
+ return length;
+ }
+
+ /**
+ * pop.
+ *
+ * @param length a int.
+ * @return a int.
+ */
+ public int pop(int length) {
+ if (length == 0)
+ return 0;
+ if (size == 0)
+ throw new ArrayIndexOutOfBoundsException(-1);
+
+ if (length > size)
+ length = size;
+
+ size -= length;
+
+ if (size == 0) {
+ head = -1;
+ tail = 0;
+ } else
+ head = (head + length) % queue.length;
+
+ return length;
+ }
+
+ /**
+ * popString.
+ *
+ * @param length a int.
+ * @param charset a {@link Charset} object.
+ * @return a {@link String} object.
+ */
+ public String popString(int length, Charset charset) {
+ byte[] b = new byte[length];
+ pop(b);
+ return new String(b, charset);
+ }
+
+ /**
+ * popAll.
+ *
+ * @return an array of {@link byte} objects.
+ */
+ public byte[] popAll() {
+ byte[] data = new byte[size];
+ pop(data, 0, data.length);
+ return data;
+ }
+
+ /**
+ * write.
+ *
+ * @param out a {@link OutputStream} object.
+ * @throws IOException if any.
+ */
+ public void write(OutputStream out) throws IOException {
+ write(out, size);
+ }
+
+ /**
+ * write.
+ *
+ * @param out a {@link OutputStream} object.
+ * @param length a int.
+ * @throws IOException if any.
+ */
+ public void write(OutputStream out, int length) throws IOException {
+ if (length == 0)
+ return;
+ if (size == 0)
+ throw new ArrayIndexOutOfBoundsException(-1);
+
+ if (length > size)
+ length = size;
+
+ int firstCopyLen = queue.length - head;
+ if (length < firstCopyLen)
+ firstCopyLen = length;
+
+ out.write(queue, head, firstCopyLen);
+ if (firstCopyLen < length)
+ out.write(queue, 0, length - firstCopyLen);
+
+ size -= length;
+
+ if (size == 0) {
+ head = -1;
+ tail = 0;
+ } else
+ head = (head + length) % queue.length;
+ }
+
+ /**
+ * tailPop.
+ *
+ * @return a byte.
+ */
+ public byte tailPop() {
+ if (size == 0)
+ throw new ArrayIndexOutOfBoundsException(-1);
+
+ tail = (tail + queue.length - 1) % queue.length;
+ byte retval = queue[tail];
+
+ if (size == 1) {
+ head = -1;
+ tail = 0;
+ }
+
+ size--;
+
+ return retval;
+ }
+
+ /**
+ * peek.
+ *
+ * @param index a int.
+ * @return a byte.
+ */
+ public byte peek(int index) {
+ if (index >= size)
+ throw new IllegalArgumentException("index " + index + " is >= queue size " + size);
+
+ index = (index + head) % queue.length;
+ return queue[index];
+ }
+
+ /**
+ * peek.
+ *
+ * @param index a int.
+ * @param length a int.
+ * @return an array of {@link byte} objects.
+ */
+ public byte[] peek(int index, int length) {
+ byte[] result = new byte[length];
+ // TODO: use System.arraycopy instead.
+ for (int i = 0; i < length; i++)
+ result[i] = peek(index + i);
+ return result;
+ }
+
+ /**
+ * peekAll.
+ *
+ * @return an array of {@link byte} objects.
+ */
+ public byte[] peekAll() {
+ byte[] data = new byte[size];
+ peek(data, 0, data.length);
+ return data;
+ }
+
+ /**
+ * peek.
+ *
+ * @param buf an array of {@link byte} objects.
+ * @return a int.
+ */
+ public int peek(byte[] buf) {
+ return peek(buf, 0, buf.length);
+ }
+
+ /**
+ * peek.
+ *
+ * @param buf an array of {@link byte} objects.
+ * @param pos a int.
+ * @param length a int.
+ * @return a int.
+ */
+ public int peek(byte[] buf, int pos, int length) {
+ if (length == 0)
+ return 0;
+ if (size == 0)
+ throw new ArrayIndexOutOfBoundsException(-1);
+
+ if (length > size)
+ length = size;
+
+ int firstCopyLen = queue.length - head;
+ if (length < firstCopyLen)
+ firstCopyLen = length;
+
+ System.arraycopy(queue, head, buf, pos, firstCopyLen);
+ if (firstCopyLen < length)
+ System.arraycopy(queue, 0, buf, pos + firstCopyLen, length - firstCopyLen);
+
+ return length;
+ }
+
+ /**
+ * indexOf.
+ *
+ * @param b a byte.
+ * @return a int.
+ */
+ public int indexOf(byte b) {
+ return indexOf(b, 0);
+ }
+
+ /**
+ * indexOf.
+ *
+ * @param b a byte.
+ * @param start a int.
+ * @return a int.
+ */
+ public int indexOf(byte b, int start) {
+ if (start >= size)
+ return -1;
+
+ int index = (head + start) % queue.length;
+ for (int i = start; i < size; i++) {
+ if (queue[index] == b)
+ return i;
+ index = (index + 1) % queue.length;
+ }
+ return -1;
+ }
+
+ /**
+ * indexOf.
+ *
+ * @param b an array of {@link byte} objects.
+ * @return a int.
+ */
+ public int indexOf(byte[] b) {
+ return indexOf(b, 0);
+ }
+
+ /**
+ * indexOf.
+ *
+ * @param b an array of {@link byte} objects.
+ * @param start a int.
+ * @return a int.
+ */
+ public int indexOf(byte[] b, int start) {
+ if (b == null || b.length == 0)
+ throw new IllegalArgumentException("can't search for empty values");
+
+ while ((start = indexOf(b[0], start)) != -1 && start < size - b.length + 1) {
+ boolean found = true;
+ for (int i = 1; i < b.length; i++) {
+ if (peek(start + i) != b[i]) {
+ found = false;
+ break;
+ }
+ }
+
+ if (found) {
+ return start;
+ }
+
+ start++;
+ }
+
+ return -1;
+ }
+
+ /**
+ * size.
+ *
+ * @return a int.
+ */
+ public int size() {
+ return size;
+ }
+
+ /**
+ * clear.
+ */
+ public void clear() {
+ size = 0;
+ head = -1;
+ tail = 0;
+ }
+
+ private int room() {
+ return queue.length - size;
+ }
+
+ private void expand() {
+ byte[] newb = new byte[queue.length * 2];
+
+ if (head == -1) {
+ queue = newb;
+ return;
+ }
+
+ if (tail > head) {
+ System.arraycopy(queue, head, newb, head, tail - head);
+ queue = newb;
+ return;
+ }
+
+ System.arraycopy(queue, head, newb, head + queue.length, queue.length - head);
+ System.arraycopy(queue, 0, newb, 0, tail);
+ head += queue.length;
+ queue = newb;
+ }
+
+ @Override
+ public Object clone() {
+ try {
+ ByteQueue clone = (ByteQueue) super.clone();
+ // Array is mutable, so make a copy of it too.
+ clone.queue = queue.clone();
+ return clone;
+ } catch (CloneNotSupportedException e) { /* Will never happen because we're Cloneable */
+ }
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ if (size == 0)
+ return "[]";
+
+ StringBuffer sb = new StringBuffer();
+ sb.append('[');
+ sb.append(Integer.toHexString(peek(0) & 0xff));
+ for (int i = 1; i < size; i++)
+ sb.append(',').append(Integer.toHexString(peek(i) & 0xff));
+ sb.append("]");
+
+ return sb.toString();
+ }
+
+ /**
+ * dumpQueue.
+ *
+ * @return a {@link String} object.
+ */
+ public String dumpQueue() {
+ StringBuffer sb = new StringBuffer();
+
+ if (queue.length == 0)
+ sb.append("[]");
+ else {
+ sb.append('[');
+ sb.append(queue[0]);
+ for (int i = 1; i < queue.length; i++) {
+ sb.append(", ");
+ sb.append(queue[i]);
+ }
+ sb.append("]");
+ }
+
+ sb.append(", h=").append(head).append(", t=").append(tail).append(", s=").append(size);
+ return sb.toString();
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/value/ModbusValue.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/value/ModbusValue.java
new file mode 100644
index 0000000..2aaee13
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/com/serotonin/modbus4j/value/ModbusValue.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2016-present the IoT DC3 original author or authors.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.value;
+
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.code.DataType;
+import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.exception.InvalidDataConversionException;
+
+import java.math.BigInteger;
+
+/**
+ * Abstract ModbusValue class.
+ *
+ * @author Matthew Lohbihler
+ * @version 2025.9.0
+ */
+abstract public class ModbusValue {
+ private final DataType type;
+ private final Object value;
+
+ /**
+ * Constructor for ModbusValue.
+ *
+ * @param type a {@link DataType} object.
+ * @param value a {@link Object} object.
+ */
+ public ModbusValue(DataType type, Object value) {
+ this.type = type;
+ this.value = value;
+ }
+
+ /**
+ * Getter for the field type.
+ *
+ * @return a {@link DataType} object.
+ */
+ public DataType getType() {
+ return type;
+ }
+
+ /**
+ * Getter for the field value.
+ *
+ * @return a {@link Object} object.
+ */
+ public Object getValue() {
+ return value;
+ }
+
+ /**
+ * booleanValue.
+ *
+ * @return a boolean.
+ */
+ public boolean booleanValue() {
+ if (value instanceof Boolean)
+ return ((Boolean) value).booleanValue();
+ throw new InvalidDataConversionException("Can't convert " + value.getClass() + " to boolean");
+ }
+
+ /**
+ * intValue.
+ *
+ * @return a int.
+ */
+ public int intValue() {
+ if (value instanceof Integer)
+ return ((Integer) value).intValue();
+ if (value instanceof Short)
+ return ((Short) value).shortValue() & 0xffff;
+ throw new InvalidDataConversionException("Can't convert " + value.getClass() + " to int");
+ }
+
+ /**
+ * longValue.
+ *
+ * @return a long.
+ */
+ public long longValue() {
+ if (value instanceof Long)
+ return ((Long) value).longValue();
+ if (value instanceof Integer)
+ return ((Integer) value).intValue() & 0xffffffff;
+ if (value instanceof Short)
+ return ((Short) value).shortValue() & 0xffff;
+ throw new InvalidDataConversionException("Can't convert " + value.getClass() + " to long");
+ }
+
+ /**
+ * bigIntegerValue.
+ *
+ * @return a {@link BigInteger} object.
+ */
+ public BigInteger bigIntegerValue() {
+ if (value instanceof BigInteger)
+ return (BigInteger) value;
+ if (value instanceof Long)
+ return BigInteger.valueOf(((Long) value).longValue());
+ if (value instanceof Integer)
+ return BigInteger.valueOf(((Integer) value).intValue() & 0xffffffff);
+ if (value instanceof Short)
+ return BigInteger.valueOf(((Short) value).shortValue() & 0xffff);
+ throw new InvalidDataConversionException("Can't convert " + value.getClass() + " to BigInteger");
+ }
+
+ /**
+ * floatValue.
+ *
+ * @return a float.
+ */
+ public float floatValue() {
+ if (value instanceof Float)
+ return ((Float) value).floatValue();
+ throw new InvalidDataConversionException("Can't convert " + value.getClass() + " to float");
+ }
+
+ /**
+ * doubleValue.
+ *
+ * @return a double.
+ */
+ public double doubleValue() {
+ if (value instanceof Double)
+ return ((Double) value).doubleValue();
+ if (value instanceof Float)
+ return ((Float) value).doubleValue();
+ throw new InvalidDataConversionException("Can't convert " + value.getClass() + " to float");
+ }
+}
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/package-info.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/package-info.java
new file mode 100644
index 0000000..b00de56
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * modbus-tcp通信协议驱动
+ * @author: lyd
+ * @date: 2026/3/3
+ */
+package org.nl.iot.core.driver.protocol.modbustcp;
\ No newline at end of file
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/package-info.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/package-info.java
new file mode 100644
index 0000000..e4d411e
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * 所有的通信协议驱动
+ * @author: lyd
+ * @date: 2026/3/3
+ */
+package org.nl.iot.core.driver.protocol;
\ No newline at end of file
diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/service/DriverCustomService.java b/nl-iot/src/main/java/org/nl/iot/core/driver/service/DriverCustomService.java
new file mode 100644
index 0000000..ba4fc5d
--- /dev/null
+++ b/nl-iot/src/main/java/org/nl/iot/core/driver/service/DriverCustomService.java
@@ -0,0 +1,63 @@
+package org.nl.iot.core.driver.service;
+
+import org.nl.iot.core.driver.bo.AttributeBO;
+import org.nl.iot.core.driver.entity.RValue;
+import org.nl.iot.modular.iot.entity.IotConfig;
+import org.nl.iot.modular.iot.entity.IotConnect;
+
+import java.util.Map;
+
+/**
+ * 自定义通信协议驱动服务
+ *
+ * 用于描述驱动的核心自定义行为逻辑, 包括初始化、调度、自定义事件、读写操作的相关功能。
+ *
+ * 具体的通信协议驱动各自实现
+ * @author: lyd
+ * @date: 2026/3/2
+ */
+public interface DriverCustomService {
+ /**
+ * 初始化
+ */
+ void initial();
+
+ /**
+ * 自定义调度
+ */
+ void schedule();
+
+ // todo驱动事件,暂时不需要
+
+ /**
+ * 执行读操作
+ *
+ * 该接口用于从指定设备中读取位号的数据。由于设备类型和通信协议的差异, 读取操作可能无法直接执行, 请根据实际情况灵活处理。
+ *
+ * 注意: 读取操作可能会抛出异常, 调用方需做好异常处理。
+ *
+ * @param driverConfig 驱动属性配置, 包含驱动相关的配置信息
+ * @param pointConfig 位号属性配置, 包含位号相关的配置信息
+ * @param device 设备对象, 包含设备的基本信息和属性
+ * @param point 位号对象, 包含位号的基本信息和属性
+ * @return 返回读取到的数据, 封装在 {@link RValue} 对象中
+ */
+ RValue read(Map driverConfig, Map pointConfig, IotConnect device, IotConfig point);
+
+ /**
+ * 执行写操作
+ *
+ * 该接口用于向指定设备中的位号写入数据。由于设备类型和通信协议的差异, 写入操作可能无法直接执行, 请根据实际情况灵活处理。
+ *
+ * 注意: 写入操作可能会抛出异常, 调用方需做好异常处理。
+ *
+ * @param driverConfig 驱动属性配置, 包含驱动相关的配置信息
+ * @param pointConfig 位号属性配置, 包含位号相关的配置信息
+ * @param device 设备对象, 包含设备的基本信息和属性
+ * @param point 位号对象, 包含位号的基本信息和属性
+ * @param wValue 待写入的数据, 封装在 {@link WValue} 对象中
+ * @return 返回写入操作是否成功, 若成功则返回 {@code true}, 否则返回 {@code false} 或抛出异常
+ */
+// Boolean write(Map driverConfig, Map pointConfig, IotConnect device, IotConfig point, WValue wValue);
+
+}
diff --git a/nl-web-app/src/test/java/org/nl/ApiTest.java b/nl-web-app/src/test/java/org/nl/ApiTest.java
new file mode 100644
index 0000000..552b2fc
--- /dev/null
+++ b/nl-web-app/src/test/java/org/nl/ApiTest.java
@@ -0,0 +1,20 @@
+package org.nl;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+/**
+ *
+ * @author: lyd
+ * @date: 2026/3/3
+ */
+@RunWith(SpringRunner.class)
+@SpringBootTest(classes = Application.class)
+public class ApiTest {
+ @Test
+ public void modbusTest() {
+
+ }
+}