package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusByteOrderEnum; import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusRawDataTypeEnum; import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrame; import lombok.experimental.UtilityClass; import lombok.extern.slf4j.Slf4j; import java.math.BigDecimal; import java.math.RoundingMode; import java.nio.ByteBuffer; import java.nio.ByteOrder; /** * IoT Modbus 协议工具类 *
* 提供 Modbus 协议全链路能力: *
* FC01(读线圈)和 FC03(读保持寄存器)支持写操作; * FC02(读离散输入)和 FC04(读输入寄存器)为只读。 * * @param readFunctionCode 读功能码(FC01-04) * @return 是否支持写操作 */ @SuppressWarnings("BooleanMethodIsAlwaysInverted") public static boolean isWritable(int readFunctionCode) { return readFunctionCode == FC_READ_COILS || readFunctionCode == FC_READ_HOLDING_REGISTERS; } /** * 获取单写功能码 *
* FC01(读线圈)→ FC05(写单个线圈); * FC03(读保持寄存器)→ FC06(写单个寄存器); * 其他返回 null(不支持写)。 * * @param readFunctionCode 读功能码 * @return 单写功能码,不支持写时返回 null */ @SuppressWarnings("EnhancedSwitchMigration") public static Integer getWriteSingleFunctionCode(int readFunctionCode) { switch (readFunctionCode) { case FC_READ_COILS: return FC_WRITE_SINGLE_COIL; case FC_READ_HOLDING_REGISTERS: return FC_WRITE_SINGLE_REGISTER; default: return null; } } /** * 获取多写功能码 *
* FC01(读线圈)→ FC15(写多个线圈); * FC03(读保持寄存器)→ FC16(写多个寄存器); * 其他返回 null(不支持写)。 * * @param readFunctionCode 读功能码 * @return 多写功能码,不支持写时返回 null */ @SuppressWarnings("EnhancedSwitchMigration") public static Integer getWriteMultipleFunctionCode(int readFunctionCode) { switch (readFunctionCode) { case FC_READ_COILS: return FC_WRITE_MULTIPLE_COILS; case FC_READ_HOLDING_REGISTERS: return FC_WRITE_MULTIPLE_REGISTERS; default: return null; } } // ==================== CRC16 工具 ==================== /** * 计算 CRC-16/MODBUS * * @param data 数据 * @param length 计算长度 * @return CRC16 值 */ public static int calculateCrc16(byte[] data, int length) { int crc = 0xFFFF; for (int i = 0; i < length; i++) { crc ^= (data[i] & 0xFF); for (int j = 0; j < 8; j++) { if ((crc & 0x0001) != 0) { crc >>= 1; crc ^= 0xA001; } else { crc >>= 1; } } } return crc; } /** * 校验 CRC16 * * @param data 包含 CRC 的完整数据 * @return 校验是否通过 */ public static boolean verifyCrc16(byte[] data) { if (data.length < 3) { return false; } int computed = calculateCrc16(data, data.length - 2); int received = (data[data.length - 2] & 0xFF) | ((data[data.length - 1] & 0xFF) << 8); return computed == received; } // ==================== 数据转换 ==================== /** * 将原始值转换为物模型属性值 * * @param rawValues 原始值数组(寄存器值或线圈值) * @param point 点位配置 * @return 转换后的属性值 */ public static Object convertToPropertyValue(int[] rawValues, IotModbusPointRespDTO point) { if (ArrayUtil.isEmpty(rawValues)) { return null; } String rawDataType = point.getRawDataType(); String byteOrder = point.getByteOrder(); BigDecimal scale = ObjectUtil.defaultIfNull(point.getScale(), BigDecimal.ONE); // 1. 根据原始数据类型解析原始数值 Number rawNumber = parseRawValue(rawValues, rawDataType, byteOrder); if (rawNumber == null) { return null; } // 2. 应用缩放因子:实际值 = 原始值 × scale BigDecimal actualValue = new BigDecimal(rawNumber.toString()).multiply(scale); // 3. 根据数据类型返回合适的 Java 类型 return formatValue(actualValue, rawDataType); } /** * 将物模型属性值转换为原始寄存器值 * * @param propertyValue 属性值 * @param point 点位配置 * @return 原始值数组 */ public static int[] convertToRawValues(Object propertyValue, IotModbusPointRespDTO point) { if (propertyValue == null) { return new int[0]; } String rawDataType = point.getRawDataType(); String byteOrder = point.getByteOrder(); BigDecimal scale = ObjectUtil.defaultIfNull(point.getScale(), BigDecimal.ONE); int registerCount = ObjectUtil.defaultIfNull(point.getRegisterCount(), 1); // 1. 转换为 BigDecimal BigDecimal actualValue = new BigDecimal(propertyValue.toString()); // 2. 应用缩放因子:原始值 = 实际值 ÷ scale BigDecimal rawValue = actualValue.divide(scale, 0, RoundingMode.HALF_UP); // 3. 根据原始数据类型编码为寄存器值 return encodeToRegisters(rawValue, rawDataType, byteOrder, registerCount); } @SuppressWarnings("EnhancedSwitchMigration") private static Number parseRawValue(int[] rawValues, String rawDataType, String byteOrder) { IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType); if (dataTypeEnum == null) { log.warn("[parseRawValue][不支持的数据类型: {}]", rawDataType); return rawValues[0]; } switch (dataTypeEnum) { case BOOLEAN: return rawValues[0] != 0 ? 1 : 0; case INT16: return (short) rawValues[0]; case UINT16: return rawValues[0] & 0xFFFF; case INT32: return parseInt32(rawValues, byteOrder); case UINT32: return parseUint32(rawValues, byteOrder); case FLOAT: return parseFloat(rawValues, byteOrder); case DOUBLE: return parseDouble(rawValues, byteOrder); default: log.warn("[parseRawValue][不支持的数据类型: {}]", rawDataType); return rawValues[0]; } } private static int parseInt32(int[] rawValues, String byteOrder) { if (rawValues.length < 2) { return rawValues[0]; } byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder); return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getInt(); } private static long parseUint32(int[] rawValues, String byteOrder) { if (rawValues.length < 2) { return rawValues[0] & 0xFFFFFFFFL; } byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder); return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getInt() & 0xFFFFFFFFL; } private static float parseFloat(int[] rawValues, String byteOrder) { if (rawValues.length < 2) { return (float) rawValues[0]; } byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder); return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getFloat(); } private static double parseDouble(int[] rawValues, String byteOrder) { if (rawValues.length < 4) { return rawValues[0]; } byte[] bytes = reorderBytes(registersToBytes(rawValues, 4), byteOrder); return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getDouble(); } private static byte[] registersToBytes(int[] registers, int count) { byte[] bytes = new byte[count * 2]; for (int i = 0; i < Math.min(registers.length, count); i++) { bytes[i * 2] = (byte) ((registers[i] >> 8) & 0xFF); bytes[i * 2 + 1] = (byte) (registers[i] & 0xFF); } return bytes; } @SuppressWarnings("EnhancedSwitchMigration") private static byte[] reorderBytes(byte[] bytes, String byteOrder) { IotModbusByteOrderEnum byteOrderEnum = IotModbusByteOrderEnum.getByOrder(byteOrder); // null 或者大端序,不需要调整 if (ObjectUtils.equalsAny(byteOrderEnum, null, IotModbusByteOrderEnum.ABCD, IotModbusByteOrderEnum.AB)) { return bytes; } // 其他字节序调整 byte[] result = new byte[bytes.length]; switch (byteOrderEnum) { case BA: // 小端序:按每 2 字节一组交换(16 位场景 [1,0],32 位场景 [1,0,3,2]) for (int i = 0; i + 1 < bytes.length; i += 2) { result[i] = bytes[i + 1]; result[i + 1] = bytes[i]; } break; case CDAB: // 大端字交换(32 位) if (bytes.length >= 4) { result[0] = bytes[2]; result[1] = bytes[3]; result[2] = bytes[0]; result[3] = bytes[1]; } break; case DCBA: // 小端序(32 位) if (bytes.length >= 4) { result[0] = bytes[3]; result[1] = bytes[2]; result[2] = bytes[1]; result[3] = bytes[0]; } break; case BADC: // 小端字交换(32 位) if (bytes.length >= 4) { result[0] = bytes[1]; result[1] = bytes[0]; result[2] = bytes[3]; result[3] = bytes[2]; } break; default: return bytes; } return result; } @SuppressWarnings("EnhancedSwitchMigration") private static int[] encodeToRegisters(BigDecimal rawValue, String rawDataType, String byteOrder, int registerCount) { IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType); if (dataTypeEnum == null) { return new int[]{rawValue.intValue()}; } switch (dataTypeEnum) { case BOOLEAN: return new int[]{rawValue.intValue() != 0 ? 1 : 0}; case INT16: case UINT16: return new int[]{rawValue.intValue() & 0xFFFF}; case INT32: return encodeInt32(rawValue.intValue(), byteOrder); case UINT32: // 使用 longValue() 避免超过 Integer.MAX_VALUE 时溢出, // 强转 int 保留低 32 位 bit pattern,写入寄存器的字节是正确的无符号值 return encodeInt32((int) rawValue.longValue(), byteOrder); case FLOAT: return encodeFloat(rawValue.floatValue(), byteOrder); case DOUBLE: return encodeDouble(rawValue.doubleValue(), byteOrder); default: return new int[]{rawValue.intValue()}; } } private static int[] encodeInt32(int value, String byteOrder) { byte[] bytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(value).array(); bytes = reorderBytes(bytes, byteOrder); return bytesToRegisters(bytes); } private static int[] encodeFloat(float value, String byteOrder) { byte[] bytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putFloat(value).array(); bytes = reorderBytes(bytes, byteOrder); return bytesToRegisters(bytes); } private static int[] encodeDouble(double value, String byteOrder) { byte[] bytes = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN).putDouble(value).array(); bytes = reorderBytes(bytes, byteOrder); return bytesToRegisters(bytes); } private static int[] bytesToRegisters(byte[] bytes) { int[] registers = new int[bytes.length / 2]; for (int i = 0; i < registers.length; i++) { registers[i] = ((bytes[i * 2] & 0xFF) << 8) | (bytes[i * 2 + 1] & 0xFF); } return registers; } @SuppressWarnings("EnhancedSwitchMigration") private static Object formatValue(BigDecimal value, String rawDataType) { IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType); if (dataTypeEnum == null) { return value; } switch (dataTypeEnum) { case BOOLEAN: return value.intValue() != 0; case INT16: case INT32: return value.intValue(); case UINT16: case UINT32: return value.longValue(); case FLOAT: return value.floatValue(); case DOUBLE: return value.doubleValue(); default: return value; } } // ==================== 帧值提取 ==================== /** * 从帧中提取寄存器值(FC01-04 读响应) * * @param frame 解码后的 Modbus 帧 * @return 寄存器值数组(int[]),失败返回 null */ @SuppressWarnings("EnhancedSwitchMigration") public static int[] extractValues(IotModbusFrame frame) { if (frame == null || frame.isException()) { return null; } byte[] pdu = frame.getPdu(); if (pdu == null || pdu.length < 1) { return null; } int functionCode = frame.getFunctionCode(); switch (functionCode) { case FC_READ_COILS: case FC_READ_DISCRETE_INPUTS: return extractCoilValues(pdu); case FC_READ_HOLDING_REGISTERS: case FC_READ_INPUT_REGISTERS: return extractRegisterValues(pdu); default: log.warn("[extractValues][不支持的功能码: {}]", functionCode); return null; } } private static int[] extractCoilValues(byte[] pdu) { if (pdu.length < 2) { return null; } int byteCount = pdu[0] & 0xFF; int bitCount = byteCount * 8; int[] values = new int[bitCount]; for (int i = 0; i < bitCount && (1 + i / 8) < pdu.length; i++) { values[i] = ((pdu[1 + i / 8] >> (i % 8)) & 0x01); } return values; } private static int[] extractRegisterValues(byte[] pdu) { if (pdu.length < 2) { return null; } int byteCount = pdu[0] & 0xFF; int registerCount = byteCount / 2; int[] values = new int[registerCount]; for (int i = 0; i < registerCount && (1 + i * 2 + 1) < pdu.length; i++) { values[i] = ((pdu[1 + i * 2] & 0xFF) << 8) | (pdu[1 + i * 2 + 1] & 0xFF); } return values; } /** * 从响应帧中提取 registerCount(通过 PDU 的 byteCount 推断) * * @param frame 解码后的 Modbus 响应帧 * @return registerCount,无法提取时返回 -1(匹配时跳过校验) */ public static int extractRegisterCountFromResponse(IotModbusFrame frame) { byte[] pdu = frame.getPdu(); if (pdu == null || pdu.length < 1) { return -1; } int byteCount = pdu[0] & 0xFF; int fc = frame.getFunctionCode(); // FC03/04 寄存器读响应:registerCount = byteCount / 2 if (fc == FC_READ_HOLDING_REGISTERS || fc == FC_READ_INPUT_REGISTERS) { return byteCount / 2; } // FC01/02 线圈/离散输入读响应:按 bit 打包有余位,无法精确反推,返回 -1 跳过校验 return -1; } // ==================== 点位查找 ==================== /** * 查找点位配置 * * @param config 设备 Modbus 配置 * @param identifier 点位标识符 * @return 匹配的点位配置,未找到返回 null */ public static IotModbusPointRespDTO findPoint(IotModbusDeviceConfigRespDTO config, String identifier) { if (config == null || StrUtil.isBlank(identifier)) { return null; } return CollUtil.findOne(config.getPoints(), p -> identifier.equals(p.getIdentifier())); } /** * 根据点位 ID 查找点位配置 * * @param config 设备 Modbus 配置 * @param pointId 点位 ID * @return 匹配的点位配置,未找到返回 null */ public static IotModbusPointRespDTO findPointById(IotModbusDeviceConfigRespDTO config, Long pointId) { if (config == null || pointId == null) { return null; } return CollUtil.findOne(config.getPoints(), p -> p.getId().equals(pointId)); } }