diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/bo/AttributeBO.java b/nl-iot/src/main/java/org/nl/iot/core/driver/bo/AttributeBO.java new file mode 100644 index 0000000..3af02da --- /dev/null +++ b/nl-iot/src/main/java/org/nl/iot/core/driver/bo/AttributeBO.java @@ -0,0 +1,176 @@ +/* + * 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.bo; + +import lombok.*; +import org.apache.commons.lang3.StringUtils; +import org.nl.common.exception.CommonException; +import org.nl.iot.core.driver.enums.AttributeTypeFlagEnum; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Objects; + +/** + * 属性配置 + * + */ +@Getter +@Setter +@Builder +@ToString +@NoArgsConstructor +@AllArgsConstructor +public class AttributeBO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 值, string, 需要根据type确定真实的数据类型 + */ + private String value; + + /** + * 类型, value type, 用于确定value的真实类型 + */ +// private AttributeTypeFlagEnum type; + + /** + * 根据类型转换数据 + * + * @param clazz T Class + * @param T + * @return T + */ + @SuppressWarnings("unchecked") +// public T getValue(Class clazz) { +// if (Objects.isNull(type)) { +// throw new CommonException("Unsupported attribute type of " + type); +// } +// if (StringUtils.isEmpty(value)) { +// throw new CommonException("Attribute value is empty"); +// } +// +// final String message = "Attribute type is: {}, can't be cast to class: {}"; +// return switch (type) { +// case STRING -> { +// if (!clazz.equals(String.class)) { +// throw new CommonException(message, type.getCode(), clazz.getName()); +// } +// yield (T) value; +// } +// case BYTE -> { +// if (!clazz.equals(Byte.class)) { +// throw new CommonException(message, type.getCode(), clazz.getName()); +// } +// yield (T) Byte.valueOf(value); +// } +// case SHORT -> { +// if (!clazz.equals(Short.class)) { +// throw new CommonException(message, type.getCode(), clazz.getName()); +// } +// yield (T) Short.valueOf(value); +// } +// case INT -> { +// if (!clazz.equals(Integer.class)) { +// throw new CommonException(message, type.getCode(), clazz.getName()); +// } +// yield (T) Integer.valueOf(value); +// } +// case LONG -> { +// if (!clazz.equals(Long.class)) { +// throw new CommonException(message, type.getCode(), clazz.getName()); +// } +// yield (T) Long.valueOf(value); +// } +// case FLOAT -> { +// if (!clazz.equals(Float.class)) { +// throw new CommonException(message, type.getCode(), clazz.getName()); +// } +// yield (T) Float.valueOf(value); +// } +// case DOUBLE -> { +// if (!clazz.equals(Double.class)) { +// throw new CommonException(message, type.getCode(), clazz.getName()); +// } +// yield (T) Double.valueOf(value); +// } +// case BOOLEAN -> { +// if (!clazz.equals(Boolean.class)) { +// throw new CommonException(message, type.getCode(), clazz.getName()); +// } +// yield (T) Boolean.valueOf(value); +// } +// }; +// } + + /** + * 将私有变量value转换为指定的基础数据类型 + * @param clazz 目标类型(仅支持String/Byte/Short/Integer/Long/Double/Float/Boolean) + * @return 转换后的目标类型实例 + * @throws CommonException 类型不支持或转换失败时抛出 + */ + public T getValueByClass(Class clazz) { + // 先校验原始值是否为null(根据业务需求,也可调整为允许null转换) + if (value == null) { + throw new CommonException("原始值value为null,无法转换为" + clazz.getSimpleName()); + } + + // 去除字符串首尾空格(可选,根据业务需求决定是否保留) + String trimmedValue = value.trim(); + if (trimmedValue.isEmpty()) { + throw new CommonException("原始值value为空字符串,无法转换为" + clazz.getSimpleName()); + } + + // 按类型分支处理转换逻辑 + try { + if (clazz == String.class) { + return clazz.cast(value); // String类型直接返回原字符串 + } else if (clazz == Byte.class || clazz == byte.class) { + return clazz.cast(Byte.parseByte(trimmedValue)); + } else if (clazz == Short.class || clazz == short.class) { + return clazz.cast(Short.parseShort(trimmedValue)); + } else if (clazz == Integer.class || clazz == int.class) { + return clazz.cast(Integer.parseInt(trimmedValue)); + } else if (clazz == Long.class || clazz == long.class) { + return clazz.cast(Long.parseLong(trimmedValue)); + } else if (clazz == Double.class || clazz == double.class) { + return clazz.cast(Double.parseDouble(trimmedValue)); + } else if (clazz == Float.class || clazz == float.class) { + return clazz.cast(Float.parseFloat(trimmedValue)); + } else if (clazz == Boolean.class || clazz == boolean.class) { + // 严格匹配布尔值:仅"true"/"false"(忽略大小写),避免"1"/"0"误判 + if ("true".equalsIgnoreCase(trimmedValue)) { + return clazz.cast(Boolean.TRUE); + } else if ("false".equalsIgnoreCase(trimmedValue)) { + return clazz.cast(Boolean.FALSE); + } else { + throw new NumberFormatException("布尔值仅支持\"true\"/\"false\"(忽略大小写),当前值:" + trimmedValue); + } + } else { + // 不支持的类型 + throw new CommonException("不支持的转换类型:" + clazz.getSimpleName() + + ",仅支持String/Byte/Short/Integer/Long/Double/Float/Boolean"); + } + } catch (NumberFormatException e) { + // 转换失败(如字符串非数字、布尔值格式错误) + throw new CommonException("转换失败:无法将值\"" + value + "\"转换为" + clazz.getSimpleName(), e); + } + } +} diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/entity/RValue.java b/nl-iot/src/main/java/org/nl/iot/core/driver/entity/RValue.java new file mode 100644 index 0000000..26043da --- /dev/null +++ b/nl-iot/src/main/java/org/nl/iot/core/driver/entity/RValue.java @@ -0,0 +1,42 @@ +package org.nl.iot.core.driver.entity; + +import lombok.*; +import org.nl.iot.modular.iot.entity.IotConfig; +import org.nl.iot.modular.iot.entity.IotConnect; + +/** + * 读数据的值 + * @author: lyd + * @date: 2026/3/2 + */ +@Getter +@Setter +@Builder +@ToString +@NoArgsConstructor +@AllArgsConstructor +public class RValue { + /** + * 设备 + */ +// private DeviceBO device; + + /** + * 位号 - 子设备 + */ +// private PointBO point; + /** + * 配置 + */ + private IotConfig config; + + /** + * 连接参数 + */ + private IotConnect connect; + + /** + * 值, string, 需要根据type确定真实的数据类型 + */ + private String value; +} diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/enums/AttributeTypeFlagEnum.java b/nl-iot/src/main/java/org/nl/iot/core/driver/enums/AttributeTypeFlagEnum.java new file mode 100644 index 0000000..a12a15b --- /dev/null +++ b/nl-iot/src/main/java/org/nl/iot/core/driver/enums/AttributeTypeFlagEnum.java @@ -0,0 +1,129 @@ +/* + * 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.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; +import java.util.Optional; + +/** + * 通用属性类型标识枚举 + * + * @author pnoker + * @version 2025.9.0 + * @since 2022.1.0 + */ +@Getter +@AllArgsConstructor +public enum AttributeTypeFlagEnum { + /** + * 字符串 + */ + STRING((byte) 0, "string", "字符串"), + + /** + * 字节 + */ + BYTE((byte) 1, "byte", "字节"), + + /** + * 短整数 + */ + SHORT((byte) 2, "short", "短整数"), + + /** + * 整数 + */ + INT((byte) 3, "int", "整数"), + + /** + * 长整数 + */ + LONG((byte) 4, "long", "长整数"), + + /** + * 浮点数 + */ + FLOAT((byte) 5, "float", "浮点数"), + + /** + * 双精度浮点数 + */ + DOUBLE((byte) 6, "double", "双精度浮点数"), + + /** + * 布尔量 + */ + BOOLEAN((byte) 7, "boolean", "布尔量"), + ; + + /** + * 索引 + */ + @EnumValue + private final Byte index; + + /** + * 编码 + */ + private final String code; + + /** + * 内容 + */ + private final String remark; + + /** + * 根据枚举索引获取枚举 + * + * @param index 索引 + * @return {@link AttributeTypeFlagEnum} + */ + public static AttributeTypeFlagEnum ofIndex(Byte index) { + Optional any = Arrays.stream(AttributeTypeFlagEnum.values()).filter(type -> type.getIndex().equals(index)).findFirst(); + return any.orElse(null); + } + + /** + * 根据枚举编码获取枚举 + * + * @param code 编码 + * @return {@link AttributeTypeFlagEnum} + */ + public static AttributeTypeFlagEnum ofCode(String code) { + Optional any = Arrays.stream(AttributeTypeFlagEnum.values()).filter(type -> type.getCode().equals(code)).findFirst(); + return any.orElse(null); + } + + /** + * 根据枚举内容获取枚举 + * + * @param name 枚举内容 + * @return {@link AttributeTypeFlagEnum} + */ + public static AttributeTypeFlagEnum ofName(String name) { + try { + return valueOf(name); + } catch (IllegalArgumentException e) { + return null; + } + } +} diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/enums/PointTypeFlagEnum.java b/nl-iot/src/main/java/org/nl/iot/core/driver/enums/PointTypeFlagEnum.java new file mode 100644 index 0000000..c1d1f64 --- /dev/null +++ b/nl-iot/src/main/java/org/nl/iot/core/driver/enums/PointTypeFlagEnum.java @@ -0,0 +1,129 @@ +/* + * 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.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; +import java.util.Optional; + +/** + * 通用位号类型标识枚举 + * + * @author pnoker + * @version 2025.9.0 + * @since 2022.1.0 + */ +@Getter +@AllArgsConstructor +public enum PointTypeFlagEnum { + /** + * 字符串 + */ + STRING((byte) 0, "string", "字符串"), + + /** + * 字节 + */ + BYTE((byte) 1, "byte", "字节"), + + /** + * 短整数 + */ + SHORT((byte) 2, "short", "短整数"), + + /** + * 整数 + */ + INT((byte) 3, "int", "整数"), + + /** + * 长整数 + */ + LONG((byte) 4, "long", "长整数"), + + /** + * 浮点数 + */ + FLOAT((byte) 5, "float", "浮点数"), + + /** + * 双精度浮点数 + */ + DOUBLE((byte) 6, "double", "双精度浮点数"), + + /** + * 布尔量 + */ + BOOLEAN((byte) 7, "boolean", "布尔量"), + ; + + /** + * 索引 + */ + @EnumValue + private final Byte index; + + /** + * 编码 + */ + private final String code; + + /** + * 内容 + */ + private final String remark; + + /** + * 根据枚举索引获取枚举 + * + * @param index 索引 + * @return {@link PointTypeFlagEnum} + */ + public static PointTypeFlagEnum ofIndex(Byte index) { + Optional any = Arrays.stream(PointTypeFlagEnum.values()).filter(type -> type.getIndex().equals(index)).findFirst(); + return any.orElse(null); + } + + /** + * 根据枚举编码获取枚举 + * + * @param code 编码 + * @return {@link PointTypeFlagEnum} + */ + public static PointTypeFlagEnum ofCode(String code) { + Optional any = Arrays.stream(PointTypeFlagEnum.values()).filter(type -> type.getCode().equals(code)).findFirst(); + return any.orElse(null); + } + + /** + * 根据枚举内容获取枚举 + * + * @param name 枚举内容 + * @return {@link PointTypeFlagEnum} + */ + public static PointTypeFlagEnum ofName(String name) { + try { + return valueOf(name); + } catch (IllegalArgumentException e) { + return null; + } + } +} diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/package-info.java b/nl-iot/src/main/java/org/nl/iot/core/driver/package-info.java new file mode 100644 index 0000000..6f8279b --- /dev/null +++ b/nl-iot/src/main/java/org/nl/iot/core/driver/package-info.java @@ -0,0 +1,7 @@ +/** + * 驱动适配包 + * 实际上是通信协议 + * @author: lyd + * @date: 2026/3/2 + */ +package org.nl.iot.core.driver; \ No newline at end of file diff --git a/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/ModBusProtocolDriverImpl.java b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/ModBusProtocolDriverImpl.java new file mode 100644 index 0000000..e66f6e0 --- /dev/null +++ b/nl-iot/src/main/java/org/nl/iot/core/driver/protocol/modbustcp/ModBusProtocolDriverImpl.java @@ -0,0 +1,204 @@ +package org.nl.iot.core.driver.protocol.modbustcp; + +import lombok.extern.slf4j.Slf4j; +import org.nl.common.exception.CommonException; +import org.nl.iot.core.driver.bo.AttributeBO; +import org.nl.iot.core.driver.entity.RValue; +import org.nl.iot.core.driver.enums.PointTypeFlagEnum; +import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ModbusFactory; +import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.ModbusMaster; +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.ErrorResponseException; +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.IpParameters; +import org.nl.iot.core.driver.protocol.modbustcp.com.serotonin.modbus4j.locator.BaseLocator; +import org.nl.iot.core.driver.service.DriverCustomService; +import org.nl.iot.modular.iot.entity.IotConfig; +import org.nl.iot.modular.iot.entity.IotConnect; +import org.springframework.stereotype.Service; + +import java.util.Map; +import java.util.Objects; + +/** + * modbus-tcp通信协议的驱动自定义服务实现类 + * @author: lyd + * @date: 2026/3/3 + */ +@Slf4j +@Service +public class ModBusProtocolDriverImpl implements DriverCustomService { + + static ModbusFactory modbusFactory; + + static { + modbusFactory = new ModbusFactory(); + } + private Map connectMap; + + @Override + public void initial() { + + } + + @Override + public void schedule() { + + } + + @Override + public RValue read(Map driverConfig, Map 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: + *
    + *
  1. Send a message for which no reply is expected, e.g. a broadcast.
  2. + *
  3. Send a message and wait for a response with timeout and retries.
  4. + *
  5. Listen for unsolicited requests.
  6. + *
+ * + * @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() { + + } +}