2026-06-24 f4bd1f3c89d906131495a0aca5aaf82966378510
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
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 协议工具类
 * <p>
 * 提供 Modbus 协议全链路能力:
 * <ul>
 *   <li>协议常量:功能码(FC01~FC16)、异常掩码等</li>
 *   <li>功能码判断:读/写/异常分类、可写判断、写功能码映射</li>
 *   <li>CRC-16/MODBUS 计算和校验</li>
 *   <li>数据转换:原始值 ↔ 物模型属性值({@link #convertToPropertyValue} / {@link #convertToRawValues})</li>
 *   <li>帧值提取:从 Modbus 帧提取寄存器/线圈值({@link #extractValues})</li>
 *   <li>点位查找({@link #findPoint})</li>
 * </ul>
 *
 * @author 芋道源码
 */
@UtilityClass
@Slf4j
public class IotModbusCommonUtils {
 
    /** FC01: 读线圈 */
    public static final int FC_READ_COILS = 1;
    /** FC02: 读离散输入 */
    public static final int FC_READ_DISCRETE_INPUTS = 2;
    /** FC03: 读保持寄存器 */
    public static final int FC_READ_HOLDING_REGISTERS = 3;
    /** FC04: 读输入寄存器 */
    public static final int FC_READ_INPUT_REGISTERS = 4;
 
    /** FC05: 写单个线圈 */
    public static final int FC_WRITE_SINGLE_COIL = 5;
    /** FC06: 写单个寄存器 */
    public static final int FC_WRITE_SINGLE_REGISTER = 6;
    /** FC15: 写多个线圈 */
    public static final int FC_WRITE_MULTIPLE_COILS = 15;
    /** FC16: 写多个寄存器 */
    public static final int FC_WRITE_MULTIPLE_REGISTERS = 16;
 
    /**
     * 异常响应掩码:响应帧的功能码最高位为 1 时,表示异常响应
     * 例如:请求 FC=0x03,异常响应 FC=0x83(0x03 | 0x80)
     */
    public static final int FC_EXCEPTION_MASK = 0x80;
 
    /**
     * 功能码掩码:用于从异常响应中提取原始功能码
     * 例如:异常 FC=0x83,原始 FC = 0x83 & 0x7F = 0x03
     */
    public static final int FC_MASK = 0x7F;
 
    // ==================== 功能码分类判断 ====================
 
    /**
     * 判断是否为读响应(FC01-04)
     */
    public static boolean isReadResponse(int functionCode) {
        return functionCode >= FC_READ_COILS && functionCode <= FC_READ_INPUT_REGISTERS;
    }
 
    /**
     * 判断是否为写响应(FC05/06/15/16)
     */
    public static boolean isWriteResponse(int functionCode) {
        return functionCode == FC_WRITE_SINGLE_COIL || functionCode == FC_WRITE_SINGLE_REGISTER
                || functionCode == FC_WRITE_MULTIPLE_COILS || functionCode == FC_WRITE_MULTIPLE_REGISTERS;
    }
 
    /**
     * 判断是否为异常响应
     */
    public static boolean isExceptionResponse(int functionCode) {
        return (functionCode & FC_EXCEPTION_MASK) != 0;
    }
 
    /**
     * 从异常响应中提取原始功能码
     */
    public static int extractOriginalFunctionCode(int exceptionFunctionCode) {
        return exceptionFunctionCode & FC_MASK;
    }
 
    /**
     * 判断读功能码是否支持写操作
     * <p>
     * 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;
    }
 
    /**
     * 获取单写功能码
     * <p>
     * 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;
        }
    }
 
    /**
     * 获取多写功能码
     * <p>
     * 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));
    }
 
}