Commit 3ab33527e3d3ea0c7a11789d9585e3079e9ee46d

Authored by YunaiV
1 parent 346ae3ff

feat(iot):modbus-tcp-slave 整体代码进一步优化

Showing 17 changed files with 427 additions and 479 deletions
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java
@@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.iot.dal.dataobject.device; @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.iot.dal.dataobject.device;
3 import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; 3 import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
4 import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; 4 import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO;
5 import cn.iocoder.yudao.module.iot.core.enums.IotModbusByteOrderEnum; 5 import cn.iocoder.yudao.module.iot.core.enums.IotModbusByteOrderEnum;
6 -import cn.iocoder.yudao.module.iot.core.enums.IotModbusFunctionCodeEnum;  
7 import cn.iocoder.yudao.module.iot.core.enums.IotModbusRawDataTypeEnum; 6 import cn.iocoder.yudao.module.iot.core.enums.IotModbusRawDataTypeEnum;
8 import com.baomidou.mybatisplus.annotation.KeySequence; 7 import com.baomidou.mybatisplus.annotation.KeySequence;
9 import com.baomidou.mybatisplus.annotation.TableId; 8 import com.baomidou.mybatisplus.annotation.TableId;
@@ -62,7 +61,7 @@ public class IotDeviceModbusPointDO extends TenantBaseDO { @@ -62,7 +61,7 @@ public class IotDeviceModbusPointDO extends TenantBaseDO {
62 /** 61 /**
63 * Modbus 功能码 62 * Modbus 功能码
64 * 63 *
65 - * 枚举 {@link IotModbusFunctionCodeEnum} 64 + * 取值范围:FC01-04(读线圈、读离散输入、读保持寄存器、读输入寄存器)
66 */ 65 */
67 private Integer functionCode; 66 private Integer functionCode;
68 /** 67 /**
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusPointRespDTO.java
1 package cn.iocoder.yudao.module.iot.core.biz.dto; 1 package cn.iocoder.yudao.module.iot.core.biz.dto;
2 2
3 import cn.iocoder.yudao.module.iot.core.enums.IotModbusByteOrderEnum; 3 import cn.iocoder.yudao.module.iot.core.enums.IotModbusByteOrderEnum;
4 -import cn.iocoder.yudao.module.iot.core.enums.IotModbusFunctionCodeEnum;  
5 import cn.iocoder.yudao.module.iot.core.enums.IotModbusRawDataTypeEnum; 4 import cn.iocoder.yudao.module.iot.core.enums.IotModbusRawDataTypeEnum;
6 import lombok.Data; 5 import lombok.Data;
7 6
@@ -33,7 +32,7 @@ public class IotModbusPointRespDTO { @@ -33,7 +32,7 @@ public class IotModbusPointRespDTO {
33 /** 32 /**
34 * Modbus 功能码 33 * Modbus 功能码
35 * 34 *
36 - * 枚举 {@link IotModbusFunctionCodeEnum} 35 + * 取值范围:FC01-04(读线圈、读离散输入、读保持寄存器、读输入寄存器)
37 */ 36 */
38 private Integer functionCode; 37 private Integer functionCode;
39 /** 38 /**
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotModbusFunctionCodeEnum.java deleted
1 -package cn.iocoder.yudao.module.iot.core.enums;  
2 -  
3 -import cn.iocoder.yudao.framework.common.core.ArrayValuable;  
4 -import lombok.Getter;  
5 -import lombok.RequiredArgsConstructor;  
6 -  
7 -import java.util.Arrays;  
8 -  
9 -/**  
10 - * IoT Modbus 功能码枚举  
11 - *  
12 - * @author 芋道源码  
13 - */  
14 -@Getter  
15 -@RequiredArgsConstructor  
16 -public enum IotModbusFunctionCodeEnum implements ArrayValuable<Integer> {  
17 -  
18 - READ_COILS(1, "读线圈", true, 5, 15),  
19 - READ_DISCRETE_INPUTS(2, "读离散输入", false, null, null),  
20 - READ_HOLDING_REGISTERS(3, "读保持寄存器", true, 6, 16),  
21 - READ_INPUT_REGISTERS(4, "读输入寄存器", false, null, null);  
22 -  
23 - public static final Integer[] ARRAYS = Arrays.stream(values())  
24 - .map(IotModbusFunctionCodeEnum::getCode)  
25 - .toArray(Integer[]::new);  
26 -  
27 - /**  
28 - * 功能码  
29 - */  
30 - private final Integer code;  
31 - /**  
32 - * 名称  
33 - */  
34 - private final String name;  
35 - /**  
36 - * 是否支持写操作  
37 - */  
38 - private final Boolean writable;  
39 - /**  
40 - * 单个写功能码  
41 - */  
42 - private final Integer writeSingleCode;  
43 - /**  
44 - * 多个写功能码  
45 - */  
46 - private final Integer writeMultipleCode;  
47 -  
48 - @Override  
49 - public Integer[] array() {  
50 - return ARRAYS;  
51 - }  
52 -  
53 - public static IotModbusFunctionCodeEnum valueOf(Integer code) {  
54 - return Arrays.stream(values())  
55 - .filter(e -> e.getCode().equals(code))  
56 - .findFirst()  
57 - .orElse(null);  
58 - }  
59 -  
60 -}  
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/IotModbusUtils.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
  5 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
  6 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame;
  7 +import lombok.extern.slf4j.Slf4j;
  8 +
  9 +/**
  10 + * IoT Modbus 工具类
  11 + * <p>
  12 + * 提供:
  13 + * 1. Modbus 协议常量(功能码、掩码等)
  14 + * 2. CRC-16/MODBUS 计算和校验
  15 + * 3. 功能码分类判断
  16 + * 4. 从解码后的 ${IotModbusFrame} 中提取寄存器值(用于后续的点位翻译)
  17 + *
  18 + * @author 芋道源码
  19 + */
  20 +@Slf4j
  21 +public class IotModbusUtils {
  22 +
  23 + /** FC01: 读线圈 */
  24 + public static final int FC_READ_COILS = 1;
  25 + /** FC02: 读离散输入 */
  26 + public static final int FC_READ_DISCRETE_INPUTS = 2;
  27 + /** FC03: 读保持寄存器 */
  28 + public static final int FC_READ_HOLDING_REGISTERS = 3;
  29 + /** FC04: 读输入寄存器 */
  30 + public static final int FC_READ_INPUT_REGISTERS = 4;
  31 +
  32 + /** FC05: 写单个线圈 */
  33 + public static final int FC_WRITE_SINGLE_COIL = 5;
  34 + /** FC06: 写单个寄存器 */
  35 + public static final int FC_WRITE_SINGLE_REGISTER = 6;
  36 + /** FC15: 写多个线圈 */
  37 + public static final int FC_WRITE_MULTIPLE_COILS = 15;
  38 + /** FC16: 写多个寄存器 */
  39 + public static final int FC_WRITE_MULTIPLE_REGISTERS = 16;
  40 +
  41 + /**
  42 + * 异常响应掩码:响应帧的功能码最高位为 1 时,表示异常响应
  43 + * 例如:请求 FC=0x03,异常响应 FC=0x83(0x03 | 0x80)
  44 + */
  45 + public static final int FC_EXCEPTION_MASK = 0x80;
  46 +
  47 + /**
  48 + * 功能码掩码:用于从异常响应中提取原始功能码
  49 + * 例如:异常 FC=0x83,原始 FC = 0x83 & 0x7F = 0x03
  50 + */
  51 + public static final int FC_MASK = 0x7F;
  52 +
  53 + // ==================== 功能码分类判断 ====================
  54 +
  55 + /**
  56 + * 判断是否为读响应(FC01-04)
  57 + */
  58 + public static boolean isReadResponse(int functionCode) {
  59 + return functionCode >= FC_READ_COILS && functionCode <= FC_READ_INPUT_REGISTERS;
  60 + }
  61 +
  62 + /**
  63 + * 判断是否为写响应(FC05/06/15/16)
  64 + */
  65 + public static boolean isWriteResponse(int functionCode) {
  66 + return functionCode == FC_WRITE_SINGLE_COIL || functionCode == FC_WRITE_SINGLE_REGISTER
  67 + || functionCode == FC_WRITE_MULTIPLE_COILS || functionCode == FC_WRITE_MULTIPLE_REGISTERS;
  68 + }
  69 +
  70 + /**
  71 + * 判断是否为异常响应
  72 + */
  73 + public static boolean isExceptionResponse(int functionCode) {
  74 + return (functionCode & FC_EXCEPTION_MASK) != 0;
  75 + }
  76 +
  77 + /**
  78 + * 从异常响应中提取原始功能码
  79 + */
  80 + public static int extractOriginalFunctionCode(int exceptionFunctionCode) {
  81 + return exceptionFunctionCode & FC_MASK;
  82 + }
  83 +
  84 + /**
  85 + * 判断读功能码是否支持写操作
  86 + * <p>
  87 + * FC01(读线圈)和 FC03(读保持寄存器)支持写操作;
  88 + * FC02(读离散输入)和 FC04(读输入寄存器)为只读。
  89 + *
  90 + * @param readFunctionCode 读功能码(FC01-04)
  91 + * @return 是否支持写操作
  92 + */
  93 + @SuppressWarnings("BooleanMethodIsAlwaysInverted")
  94 + public static boolean isWritable(int readFunctionCode) {
  95 + return readFunctionCode == FC_READ_COILS || readFunctionCode == FC_READ_HOLDING_REGISTERS;
  96 + }
  97 +
  98 + /**
  99 + * 获取单写功能码
  100 + * <p>
  101 + * FC01(读线圈)→ FC05(写单个线圈);
  102 + * FC03(读保持寄存器)→ FC06(写单个寄存器);
  103 + * 其他返回 null(不支持写)。
  104 + *
  105 + * @param readFunctionCode 读功能码
  106 + * @return 单写功能码,不支持写时返回 null
  107 + */
  108 + @SuppressWarnings("EnhancedSwitchMigration")
  109 + public static Integer getWriteSingleFunctionCode(int readFunctionCode) {
  110 + switch (readFunctionCode) {
  111 + case FC_READ_COILS:
  112 + return FC_WRITE_SINGLE_COIL;
  113 + case FC_READ_HOLDING_REGISTERS:
  114 + return FC_WRITE_SINGLE_REGISTER;
  115 + default:
  116 + return null;
  117 + }
  118 + }
  119 +
  120 + /**
  121 + * 获取多写功能码
  122 + * <p>
  123 + * FC01(读线圈)→ FC15(写多个线圈);
  124 + * FC03(读保持寄存器)→ FC16(写多个寄存器);
  125 + * 其他返回 null(不支持写)。
  126 + *
  127 + * @param readFunctionCode 读功能码
  128 + * @return 多写功能码,不支持写时返回 null
  129 + */
  130 + @SuppressWarnings("EnhancedSwitchMigration")
  131 + public static Integer getWriteMultipleFunctionCode(int readFunctionCode) {
  132 + switch (readFunctionCode) {
  133 + case FC_READ_COILS:
  134 + return FC_WRITE_MULTIPLE_COILS;
  135 + case FC_READ_HOLDING_REGISTERS:
  136 + return FC_WRITE_MULTIPLE_REGISTERS;
  137 + default:
  138 + return null;
  139 + }
  140 + }
  141 +
  142 + // ==================== 点位查找 ====================
  143 +
  144 + /**
  145 + * 查找点位配置
  146 + *
  147 + * @param config 设备 Modbus 配置
  148 + * @param identifier 点位标识符
  149 + * @return 匹配的点位配置,未找到返回 null
  150 + */
  151 + public static IotModbusPointRespDTO findPoint(IotModbusDeviceConfigRespDTO config, String identifier) {
  152 + return CollUtil.findOne(config.getPoints(), p -> identifier.equals(p.getIdentifier()));
  153 + }
  154 +
  155 + // ==================== CRC16 工具 ====================
  156 +
  157 + /**
  158 + * 计算 CRC-16/MODBUS
  159 + *
  160 + * @param data 数据
  161 + * @param length 计算长度
  162 + * @return CRC16 值
  163 + */
  164 + public static int calculateCrc16(byte[] data, int length) {
  165 + int crc = 0xFFFF;
  166 + for (int i = 0; i < length; i++) {
  167 + crc ^= (data[i] & 0xFF);
  168 + for (int j = 0; j < 8; j++) {
  169 + if ((crc & 0x0001) != 0) {
  170 + crc >>= 1;
  171 + crc ^= 0xA001;
  172 + } else {
  173 + crc >>= 1;
  174 + }
  175 + }
  176 + }
  177 + return crc;
  178 + }
  179 +
  180 + /**
  181 + * 校验 CRC16
  182 + *
  183 + * @param data 包含 CRC 的完整数据
  184 + * @return 校验是否通过
  185 + */
  186 + public static boolean verifyCrc16(byte[] data) {
  187 + if (data.length < 3) {
  188 + return false;
  189 + }
  190 + int computed = calculateCrc16(data, data.length - 2);
  191 + int received = (data[data.length - 2] & 0xFF) | ((data[data.length - 1] & 0xFF) << 8);
  192 + return computed == received;
  193 + }
  194 +
  195 + // ==================== 响应值提取 ====================
  196 +
  197 + /**
  198 + * 从帧中提取寄存器值(FC01-04 读响应)
  199 + *
  200 + * @param frame 解码后的 Modbus 帧
  201 + * @return 寄存器值数组(int[]),失败返回 null
  202 + */
  203 + @SuppressWarnings("EnhancedSwitchMigration")
  204 + public static int[] extractValues(IotModbusFrame frame) {
  205 + if (frame == null || frame.isException()) {
  206 + return null;
  207 + }
  208 + byte[] pdu = frame.getPdu();
  209 + if (pdu == null || pdu.length < 1) {
  210 + return null;
  211 + }
  212 +
  213 + int functionCode = frame.getFunctionCode();
  214 + switch (functionCode) {
  215 + case FC_READ_COILS:
  216 + case FC_READ_DISCRETE_INPUTS:
  217 + return extractCoilValues(pdu);
  218 + case FC_READ_HOLDING_REGISTERS:
  219 + case FC_READ_INPUT_REGISTERS:
  220 + return extractRegisterValues(pdu);
  221 + default:
  222 + log.warn("[extractValues][不支持的功能码: {}]", functionCode);
  223 + return null;
  224 + }
  225 + }
  226 +
  227 + /**
  228 + * 提取线圈/离散输入值
  229 + * PDU 格式(FC01/02 响应):[ByteCount(1)] [CoilStatus(N)]
  230 + */
  231 + private static int[] extractCoilValues(byte[] pdu) {
  232 + if (pdu.length < 2) {
  233 + return null;
  234 + }
  235 + int byteCount = pdu[0] & 0xFF;
  236 + int bitCount = byteCount * 8;
  237 + int[] values = new int[bitCount];
  238 + for (int i = 0; i < bitCount && (1 + i / 8) < pdu.length; i++) {
  239 + values[i] = ((pdu[1 + i / 8] >> (i % 8)) & 0x01);
  240 + }
  241 + return values;
  242 + }
  243 +
  244 + /**
  245 + * 提取寄存器值
  246 + * PDU 格式(FC03/04 响应):[ByteCount(1)] [RegisterData(N*2)]
  247 + */
  248 + private static int[] extractRegisterValues(byte[] pdu) {
  249 + if (pdu.length < 2) {
  250 + return null;
  251 + }
  252 + int byteCount = pdu[0] & 0xFF;
  253 + int registerCount = byteCount / 2;
  254 + int[] values = new int[registerCount];
  255 + for (int i = 0; i < registerCount && (1 + i * 2 + 1) < pdu.length; i++) {
  256 + values[i] = ((pdu[1 + i * 2] & 0xFF) << 8) | (pdu[1 + i * 2 + 1] & 0xFF);
  257 + }
  258 + return values;
  259 + }
  260 +
  261 +}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/client/IotModbusTcpClient.java
1 package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client; 1 package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client;
2 2
3 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; 3 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
4 -import cn.iocoder.yudao.module.iot.core.enums.IotModbusFunctionCodeEnum;  
5 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConnectionManager; 4 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConnectionManager;
6 import com.ghgande.j2mod.modbus.io.ModbusTCPTransaction; 5 import com.ghgande.j2mod.modbus.io.ModbusTCPTransaction;
7 import com.ghgande.j2mod.modbus.msg.*; 6 import com.ghgande.j2mod.modbus.msg.*;
@@ -12,6 +11,8 @@ import com.ghgande.j2mod.modbus.util.BitVector; @@ -12,6 +11,8 @@ import com.ghgande.j2mod.modbus.util.BitVector;
12 import io.vertx.core.Future; 11 import io.vertx.core.Future;
13 import lombok.extern.slf4j.Slf4j; 12 import lombok.extern.slf4j.Slf4j;
14 13
  14 +import static cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils.*;
  15 +
15 // TODO @AI:感觉它更像一个工具类;但是名字叫 client 很奇怪; 16 // TODO @AI:感觉它更像一个工具类;但是名字叫 client 很奇怪;
16 /** 17 /**
17 * IoT Modbus TCP 客户端 18 * IoT Modbus TCP 客户端
@@ -98,18 +99,14 @@ public class IotModbusTcpClient { @@ -98,18 +99,14 @@ public class IotModbusTcpClient {
98 */ 99 */
99 @SuppressWarnings("EnhancedSwitchMigration") 100 @SuppressWarnings("EnhancedSwitchMigration")
100 private ModbusRequest createReadRequest(Integer functionCode, Integer address, Integer count) { 101 private ModbusRequest createReadRequest(Integer functionCode, Integer address, Integer count) {
101 - IotModbusFunctionCodeEnum functionCodeEnum = IotModbusFunctionCodeEnum.valueOf(functionCode);  
102 - if (functionCodeEnum == null) {  
103 - throw new IllegalArgumentException("不支持的功能码: " + functionCode);  
104 - }  
105 - switch (functionCodeEnum) {  
106 - case READ_COILS: 102 + switch (functionCode) {
  103 + case FC_READ_COILS:
107 return new ReadCoilsRequest(address, count); 104 return new ReadCoilsRequest(address, count);
108 - case READ_DISCRETE_INPUTS: 105 + case FC_READ_DISCRETE_INPUTS:
109 return new ReadInputDiscretesRequest(address, count); 106 return new ReadInputDiscretesRequest(address, count);
110 - case READ_HOLDING_REGISTERS: 107 + case FC_READ_HOLDING_REGISTERS:
111 return new ReadMultipleRegistersRequest(address, count); 108 return new ReadMultipleRegistersRequest(address, count);
112 - case READ_INPUT_REGISTERS: 109 + case FC_READ_INPUT_REGISTERS:
113 return new ReadInputRegistersRequest(address, count); 110 return new ReadInputRegistersRequest(address, count);
114 default: 111 default:
115 throw new IllegalArgumentException("不支持的功能码: " + functionCode); 112 throw new IllegalArgumentException("不支持的功能码: " + functionCode);
@@ -119,13 +116,10 @@ public class IotModbusTcpClient { @@ -119,13 +116,10 @@ public class IotModbusTcpClient {
119 /** 116 /**
120 * 创建写入请求 117 * 创建写入请求
121 */ 118 */
  119 + @SuppressWarnings("EnhancedSwitchMigration")
122 private ModbusRequest createWriteRequest(Integer functionCode, Integer address, Integer count, int[] values) { 120 private ModbusRequest createWriteRequest(Integer functionCode, Integer address, Integer count, int[] values) {
123 - IotModbusFunctionCodeEnum functionCodeEnum = IotModbusFunctionCodeEnum.valueOf(functionCode);  
124 - if (functionCodeEnum == null) {  
125 - throw new IllegalArgumentException("不支持的功能码: " + functionCode);  
126 - }  
127 - switch (functionCodeEnum) {  
128 - case READ_COILS: // 写线圈(使用功能码 5 或 15) 121 + switch (functionCode) {
  122 + case FC_READ_COILS: // 写线圈(使用功能码 5 或 15)
129 if (count == 1) { 123 if (count == 1) {
130 return new WriteCoilRequest(address, values[0] != 0); 124 return new WriteCoilRequest(address, values[0] != 0);
131 } else { 125 } else {
@@ -135,7 +129,7 @@ public class IotModbusTcpClient { @@ -135,7 +129,7 @@ public class IotModbusTcpClient {
135 } 129 }
136 return new WriteMultipleCoilsRequest(address, bv); 130 return new WriteMultipleCoilsRequest(address, bv);
137 } 131 }
138 - case READ_HOLDING_REGISTERS: // 写保持寄存器(使用功能码 6 或 16) 132 + case FC_READ_HOLDING_REGISTERS: // 写保持寄存器(使用功能码 6 或 16)
139 if (count == 1) { 133 if (count == 1) {
140 return new WriteSingleRegisterRequest(address, new SimpleRegister(values[0])); 134 return new WriteSingleRegisterRequest(address, new SimpleRegister(values[0]));
141 } else { 135 } else {
@@ -145,8 +139,8 @@ public class IotModbusTcpClient { @@ -145,8 +139,8 @@ public class IotModbusTcpClient {
145 } 139 }
146 return new WriteMultipleRegistersRequest(address, registers); 140 return new WriteMultipleRegistersRequest(address, registers);
147 } 141 }
148 - case READ_DISCRETE_INPUTS: // 只读  
149 - case READ_INPUT_REGISTERS: // 只读 142 + case FC_READ_DISCRETE_INPUTS: // 只读
  143 + case FC_READ_INPUT_REGISTERS: // 只读
150 return null; 144 return null;
151 default: 145 default:
152 throw new IllegalArgumentException("不支持的功能码: " + functionCode); 146 throw new IllegalArgumentException("不支持的功能码: " + functionCode);
@@ -156,13 +150,10 @@ public class IotModbusTcpClient { @@ -156,13 +150,10 @@ public class IotModbusTcpClient {
156 /** 150 /**
157 * 从响应中提取值 151 * 从响应中提取值
158 */ 152 */
  153 + @SuppressWarnings("EnhancedSwitchMigration")
159 private int[] extractValues(ModbusResponse response, Integer functionCode) { 154 private int[] extractValues(ModbusResponse response, Integer functionCode) {
160 - IotModbusFunctionCodeEnum functionCodeEnum = IotModbusFunctionCodeEnum.valueOf(functionCode);  
161 - if (functionCodeEnum == null) {  
162 - throw new IllegalArgumentException("不支持的功能码: " + functionCode);  
163 - }  
164 - switch (functionCodeEnum) {  
165 - case READ_COILS: 155 + switch (functionCode) {
  156 + case FC_READ_COILS:
166 ReadCoilsResponse coilsResponse = (ReadCoilsResponse) response; 157 ReadCoilsResponse coilsResponse = (ReadCoilsResponse) response;
167 int bitCount = coilsResponse.getBitCount(); 158 int bitCount = coilsResponse.getBitCount();
168 int[] coilValues = new int[bitCount]; 159 int[] coilValues = new int[bitCount];
@@ -170,7 +161,7 @@ public class IotModbusTcpClient { @@ -170,7 +161,7 @@ public class IotModbusTcpClient {
170 coilValues[i] = coilsResponse.getCoilStatus(i) ? 1 : 0; 161 coilValues[i] = coilsResponse.getCoilStatus(i) ? 1 : 0;
171 } 162 }
172 return coilValues; 163 return coilValues;
173 - case READ_DISCRETE_INPUTS: 164 + case FC_READ_DISCRETE_INPUTS:
174 ReadInputDiscretesResponse discretesResponse = (ReadInputDiscretesResponse) response; 165 ReadInputDiscretesResponse discretesResponse = (ReadInputDiscretesResponse) response;
175 int discreteCount = discretesResponse.getBitCount(); 166 int discreteCount = discretesResponse.getBitCount();
176 int[] discreteValues = new int[discreteCount]; 167 int[] discreteValues = new int[discreteCount];
@@ -178,7 +169,7 @@ public class IotModbusTcpClient { @@ -178,7 +169,7 @@ public class IotModbusTcpClient {
178 discreteValues[i] = discretesResponse.getDiscreteStatus(i) ? 1 : 0; 169 discreteValues[i] = discretesResponse.getDiscreteStatus(i) ? 1 : 0;
179 } 170 }
180 return discreteValues; 171 return discreteValues;
181 - case READ_HOLDING_REGISTERS: 172 + case FC_READ_HOLDING_REGISTERS:
182 ReadMultipleRegistersResponse holdingResponse = (ReadMultipleRegistersResponse) response; 173 ReadMultipleRegistersResponse holdingResponse = (ReadMultipleRegistersResponse) response;
183 InputRegister[] holdingRegisters = holdingResponse.getRegisters(); 174 InputRegister[] holdingRegisters = holdingResponse.getRegisters();
184 int[] holdingValues = new int[holdingRegisters.length]; 175 int[] holdingValues = new int[holdingRegisters.length];
@@ -186,7 +177,7 @@ public class IotModbusTcpClient { @@ -186,7 +177,7 @@ public class IotModbusTcpClient {
186 holdingValues[i] = holdingRegisters[i].getValue(); 177 holdingValues[i] = holdingRegisters[i].getValue();
187 } 178 }
188 return holdingValues; 179 return holdingValues;
189 - case READ_INPUT_REGISTERS: 180 + case FC_READ_INPUT_REGISTERS:
190 ReadInputRegistersResponse inputResponse = (ReadInputRegistersResponse) response; 181 ReadInputRegistersResponse inputResponse = (ReadInputRegistersResponse) response;
191 InputRegister[] inputRegisters = inputResponse.getRegisters(); 182 InputRegister[] inputRegisters = inputResponse.getRegisters();
192 int[] inputValues = new int[inputRegisters.length]; 183 int[] inputValues = new int[inputRegisters.length];
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamHandler.java
1 package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream; 1 package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream;
2 2
3 -import cn.hutool.core.collection.CollUtil;  
4 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; 3 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; 4 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
6 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 5 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
7 -import cn.iocoder.yudao.module.iot.core.enums.IotModbusFunctionCodeEnum;  
8 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
9 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; 7 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter;
  8 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils;
10 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client.IotModbusTcpClient; 9 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.client.IotModbusTcpClient;
11 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConfigCacheService; 10 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConfigCacheService;
12 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConnectionManager; 11 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.manager.IotModbusTcpConnectionManager;
@@ -60,19 +59,19 @@ public class IotModbusTcpDownstreamHandler { @@ -60,19 +59,19 @@ public class IotModbusTcpDownstreamHandler {
60 for (Map.Entry<String, Object> entry : propertyMap.entrySet()) { 59 for (Map.Entry<String, Object> entry : propertyMap.entrySet()) {
61 String identifier = entry.getKey(); 60 String identifier = entry.getKey();
62 Object value = entry.getValue(); 61 Object value = entry.getValue();
63 - // 2.1.1 查找对应的点位配置  
64 - IotModbusPointRespDTO point = findPoint(config, identifier); 62 + // 2.1 查找对应的点位配置
  63 + IotModbusPointRespDTO point = IotModbusUtils.findPoint(config, identifier);
65 if (point == null) { 64 if (point == null) {
66 log.warn("[handle][设备 {} 没有点位配置: {}]", message.getDeviceId(), identifier); 65 log.warn("[handle][设备 {} 没有点位配置: {}]", message.getDeviceId(), identifier);
67 continue; 66 continue;
68 } 67 }
69 - // 2.1.2 检查是否支持写操作  
70 - if (!isWritable(point.getFunctionCode())) { 68 + // 2.2 检查是否支持写操作
  69 + if (!IotModbusUtils.isWritable(point.getFunctionCode())) {
71 log.warn("[handle][点位 {} 不支持写操作, 功能码={}]", identifier, point.getFunctionCode()); 70 log.warn("[handle][点位 {} 不支持写操作, 功能码={}]", identifier, point.getFunctionCode());
72 continue; 71 continue;
73 } 72 }
74 73
75 - // 2.2 执行写入 74 + // 2.3 执行写入
76 writeProperty(config, point, value); 75 writeProperty(config, point, value);
77 } 76 }
78 } 77 }
@@ -104,19 +103,4 @@ public class IotModbusTcpDownstreamHandler { @@ -104,19 +103,4 @@ public class IotModbusTcpDownstreamHandler {
104 config.getDeviceId(), point.getIdentifier(), e)); 103 config.getDeviceId(), point.getIdentifier(), e));
105 } 104 }
106 105
107 - /**  
108 - * 查找点位配置  
109 - */  
110 - private IotModbusPointRespDTO findPoint(IotModbusDeviceConfigRespDTO config, String identifier) {  
111 - return CollUtil.findOne(config.getPoints(), p -> identifier.equals(p.getIdentifier()));  
112 - }  
113 -  
114 - /**  
115 - * 检查功能码是否支持写操作  
116 - */  
117 - private boolean isWritable(Integer functionCode) {  
118 - IotModbusFunctionCodeEnum functionCodeEnum = IotModbusFunctionCodeEnum.valueOf(functionCode);  
119 - return functionCodeEnum != null && Boolean.TRUE.equals(functionCodeEnum.getWritable());  
120 - }  
121 -  
122 } 106 }
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamSubscriber.java
1 package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream; 1 package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.handler.downstream;
2 2
3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; 3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4 -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;  
5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
6 -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; 5 +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
7 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.IotModbusTcpMasterProtocol; 6 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpmaster.IotModbusTcpMasterProtocol;
8 -import lombok.RequiredArgsConstructor;  
9 import lombok.extern.slf4j.Slf4j; 7 import lombok.extern.slf4j.Slf4j;
10 8
11 -// TODO @AI:是不是可以继承 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolDownstreamSubscriber.java  
12 /** 9 /**
13 * IoT Modbus TCP 下行消息订阅器:订阅消息总线的下行消息并转发给处理器 10 * IoT Modbus TCP 下行消息订阅器:订阅消息总线的下行消息并转发给处理器
14 * 11 *
15 * @author 芋道源码 12 * @author 芋道源码
16 */ 13 */
17 -@RequiredArgsConstructor  
18 @Slf4j 14 @Slf4j
19 -public class IotModbusTcpDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> { 15 +public class IotModbusTcpDownstreamSubscriber extends IotProtocolDownstreamSubscriber {
20 16
21 - private final IotModbusTcpMasterProtocol protocol;  
22 private final IotModbusTcpDownstreamHandler downstreamHandler; 17 private final IotModbusTcpDownstreamHandler downstreamHandler;
23 - private final IotMessageBus messageBus;  
24 18
25 - /**  
26 - * 启动订阅  
27 - */  
28 - public void start() {  
29 - messageBus.register(this);  
30 - log.info("[start][Modbus TCP Master 下行消息订阅器已启动, topic={}]", getTopic());  
31 - }  
32 -  
33 - /**  
34 - * 停止订阅  
35 - */  
36 - public void stop() {  
37 - messageBus.unregister(this);  
38 - log.info("[stop][Modbus TCP Master 下行消息订阅器已停止]");  
39 - }  
40 -  
41 - @Override  
42 - public String getTopic() {  
43 - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId());  
44 - }  
45 -  
46 - @Override  
47 - public String getGroup() {  
48 - return getTopic(); // 点对点消费 19 + public IotModbusTcpDownstreamSubscriber(IotModbusTcpMasterProtocol protocol,
  20 + IotModbusTcpDownstreamHandler downstreamHandler,
  21 + IotMessageBus messageBus) {
  22 + super(protocol, messageBus);
  23 + this.downstreamHandler = downstreamHandler;
49 } 24 }
50 25
51 @Override 26 @Override
52 - public void onMessage(IotDeviceMessage message) {  
53 - log.debug("[onMessage][收到下行消息: {}]", message);  
54 - try {  
55 - downstreamHandler.handle(message);  
56 - } catch (Exception e) {  
57 - log.error("[onMessage][处理下行消息失败]", e);  
58 - } 27 + protected void handleMessage(IotDeviceMessage message) {
  28 + downstreamHandler.handle(message);
59 } 29 }
60 30
61 } 31 }
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveConfig.java
1 package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave; 1 package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave;
2 2
  3 +import jakarta.validation.constraints.Max;
3 import jakarta.validation.constraints.Min; 4 import jakarta.validation.constraints.Min;
4 import jakarta.validation.constraints.NotNull; 5 import jakarta.validation.constraints.NotNull;
5 import lombok.Data; 6 import lombok.Data;
@@ -25,7 +26,7 @@ public class IotModbusTcpSlaveConfig { @@ -25,7 +26,7 @@ public class IotModbusTcpSlaveConfig {
25 */ 26 */
26 @NotNull(message = "自定义功能码不能为空") 27 @NotNull(message = "自定义功能码不能为空")
27 @Min(value = 65, message = "自定义功能码不能小于 65") 28 @Min(value = 65, message = "自定义功能码不能小于 65")
28 - // TODO @AI:搞个范围; 29 + @Max(value = 72, message = "自定义功能码不能大于 72")
29 private Integer customFunctionCode = 65; 30 private Integer customFunctionCode = 65;
30 31
31 /** 32 /**
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrame.java
1 package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec; 1 package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec;
2 2
  3 +import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
  4 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils;
3 import lombok.Data; 5 import lombok.Data;
4 import lombok.experimental.Accessors; 6 import lombok.experimental.Accessors;
5 7
@@ -25,19 +27,19 @@ public class IotModbusFrame { @@ -25,19 +27,19 @@ public class IotModbusFrame {
25 */ 27 */
26 private byte[] pdu; 28 private byte[] pdu;
27 /** 29 /**
28 - * 事务标识符(TCP 模式特有)  
29 - *  
30 - * // TODO @AI:最好是 @某个类型独有; 30 + * 事务标识符
  31 + * <p>
  32 + * 仅 {@link IotModbusFrameFormatEnum#MODBUS_TCP} 格式有值,
31 */ 33 */
32 private Integer transactionId; 34 private Integer transactionId;
33 35
34 /** 36 /**
35 - * 是否异常响应  
36 - */  
37 - private boolean exception;  
38 - // TODO @AI:是不是要枚举一些异常;另外,是不是覆盖掉 exception;因为只要判断有异常码是不是就可以了;  
39 - /**  
40 - * 异常码(当 exception=true 时有效) 37 + * 异常码
  38 + * <p>
  39 + * 当功能码最高位为 1 时(异常响应),此字段存储异常码。
  40 + * 为 null 表示非异常响应。
  41 + *
  42 + * @see IotModbusUtils#FC_EXCEPTION_MASK
41 */ 43 */
42 private Integer exceptionCode; 44 private Integer exceptionCode;
43 45
@@ -46,4 +48,11 @@ public class IotModbusFrame { @@ -46,4 +48,11 @@ public class IotModbusFrame {
46 */ 48 */
47 private String customData; 49 private String customData;
48 50
  51 + /**
  52 + * 是否异常响应(基于 exceptionCode 是否有值判断)
  53 + */
  54 + public boolean isException() {
  55 + return exceptionCode != null;
  56 + }
  57 +
49 } 58 }
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameDecoder.java
1 package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec; 1 package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec;
2 2
3 import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; 3 import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
  4 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils;
4 import io.vertx.core.Handler; 5 import io.vertx.core.Handler;
5 import io.vertx.core.buffer.Buffer; 6 import io.vertx.core.buffer.Buffer;
6 import io.vertx.core.parsetools.RecordParser; 7 import io.vertx.core.parsetools.RecordParser;
@@ -118,11 +119,8 @@ public class IotModbusFrameDecoder { @@ -118,11 +119,8 @@ public class IotModbusFrameDecoder {
118 .setPdu(pdu) 119 .setPdu(pdu)
119 .setTransactionId(transactionId); 120 .setTransactionId(transactionId);
120 // 异常响应 121 // 异常响应
121 - // TODO @AI:0x80 看看是不是要枚举;  
122 - if ((functionCode & 0x80) != 0) {  
123 - frame.setException(true);  
124 - // TODO @AI:0x7f 看看是不是要枚举;  
125 - frame.setFunctionCode(functionCode & 0x7F); 122 + if (IotModbusUtils.isExceptionResponse(functionCode)) {
  123 + frame.setFunctionCode(IotModbusUtils.extractOriginalFunctionCode(functionCode));
126 if (pdu.length >= 1) { 124 if (pdu.length >= 1) {
127 frame.setExceptionCode(pdu[0] & 0xFF); 125 frame.setExceptionCode(pdu[0] & 0xFF);
128 } 126 }
@@ -247,7 +245,7 @@ public class IotModbusFrameDecoder { @@ -247,7 +245,7 @@ public class IotModbusFrameDecoder {
247 * 状态机流程: 245 * 状态机流程:
248 * Phase 1: fixedSizeMode(2) → 读 slaveId + functionCode 246 * Phase 1: fixedSizeMode(2) → 读 slaveId + functionCode
249 * Phase 2: 根据 functionCode 确定剩余长度: 247 * Phase 2: 根据 functionCode 确定剩余长度:
250 - * - 异常响应 (FC & 0x80):fixedSizeMode(3) → exceptionCode(1) + CRC(2) 248 + * - 异常响应 (FC & EXCEPTION_MASK):fixedSizeMode(3) → exceptionCode(1) + CRC(2)
251 * - 自定义 FC / FC01-04 响应:fixedSizeMode(1) → 读 byteCount → fixedSizeMode(byteCount + 2) 249 * - 自定义 FC / FC01-04 响应:fixedSizeMode(1) → 读 byteCount → fixedSizeMode(byteCount + 2)
252 * - FC05/06 响应:fixedSizeMode(6) → addr(2) + value(2) + CRC(2) 250 * - FC05/06 响应:fixedSizeMode(6) → addr(2) + value(2) + CRC(2)
253 * - FC15/16 响应:fixedSizeMode(6) → addr(2) + quantity(2) + CRC(2) 251 * - FC15/16 响应:fixedSizeMode(6) → addr(2) + quantity(2) + CRC(2)
@@ -283,7 +281,7 @@ public class IotModbusFrameDecoder { @@ -283,7 +281,7 @@ public class IotModbusFrameDecoder {
283 this.slaveId = bytes[0]; 281 this.slaveId = bytes[0];
284 this.functionCode = bytes[1]; 282 this.functionCode = bytes[1];
285 int fc = functionCode & 0xFF; 283 int fc = functionCode & 0xFF;
286 - if ((fc & 0x80) != 0) { 284 + if (IotModbusUtils.isExceptionResponse(fc)) {
287 // 异常响应:完整帧 = slaveId(1) + FC(1) + exceptionCode(1) + CRC(2) = 5 字节 285 // 异常响应:完整帧 = slaveId(1) + FC(1) + exceptionCode(1) + CRC(2) = 5 字节
288 // 已有 6 字节(多 1 字节),取前 5 字节组装 286 // 已有 6 字节(多 1 字节),取前 5 字节组装
289 Buffer frame = Buffer.buffer(5); 287 Buffer frame = Buffer.buffer(5);
@@ -292,7 +290,7 @@ public class IotModbusFrameDecoder { @@ -292,7 +290,7 @@ public class IotModbusFrameDecoder {
292 frame.appendBytes(bytes, 2, 3); // exceptionCode + CRC 290 frame.appendBytes(bytes, 2, 3); // exceptionCode + CRC
293 emitFrame(frame); 291 emitFrame(frame);
294 resetToHeader(); 292 resetToHeader();
295 - } else if (isReadResponse(fc) || fc == customFunctionCode) { 293 + } else if (IotModbusUtils.isReadResponse(fc) || fc == customFunctionCode) {
296 // 读响应或自定义 FC:bytes[2] = byteCount 294 // 读响应或自定义 FC:bytes[2] = byteCount
297 this.byteCount = bytes[2]; 295 this.byteCount = bytes[2];
298 int bc = byteCount & 0xFF; 296 int bc = byteCount & 0xFF;
@@ -317,7 +315,7 @@ public class IotModbusFrameDecoder { @@ -317,7 +315,7 @@ public class IotModbusFrameDecoder {
317 this.expectedDataLen = bc + 2; // byteCount 个数据 + 2 CRC 315 this.expectedDataLen = bc + 2; // byteCount 个数据 + 2 CRC
318 parser.fixedSizeMode(remaining); 316 parser.fixedSizeMode(remaining);
319 } 317 }
320 - } else if (isWriteResponse(fc)) { 318 + } else if (IotModbusUtils.isWriteResponse(fc)) {
321 // 写响应:总长 = slaveId(1) + FC(1) + addr(2) + value/qty(2) + CRC(2) = 8 字节 319 // 写响应:总长 = slaveId(1) + FC(1) + addr(2) + value/qty(2) + CRC(2) = 8 字节
322 // 已有 6 字节,还需 2 字节 320 // 已有 6 字节,还需 2 字节
323 state = STATE_WRITE_BODY; 321 state = STATE_WRITE_BODY;
@@ -358,15 +356,15 @@ public class IotModbusFrameDecoder { @@ -358,15 +356,15 @@ public class IotModbusFrameDecoder {
358 this.slaveId = header[0]; 356 this.slaveId = header[0];
359 this.functionCode = header[1]; 357 this.functionCode = header[1];
360 int fc = functionCode & 0xFF; 358 int fc = functionCode & 0xFF;
361 - if ((fc & 0x80) != 0) { 359 + if (IotModbusUtils.isExceptionResponse(fc)) {
362 // 异常响应 360 // 异常响应
363 state = STATE_EXCEPTION_BODY; 361 state = STATE_EXCEPTION_BODY;
364 parser.fixedSizeMode(3); // exceptionCode(1) + CRC(2) 362 parser.fixedSizeMode(3); // exceptionCode(1) + CRC(2)
365 - } else if (isReadResponse(fc) || fc == customFunctionCode) { 363 + } else if (IotModbusUtils.isReadResponse(fc) || fc == customFunctionCode) {
366 // 读响应或自定义 FC 364 // 读响应或自定义 FC
367 state = STATE_READ_BYTE_COUNT; 365 state = STATE_READ_BYTE_COUNT;
368 parser.fixedSizeMode(1); // byteCount 366 parser.fixedSizeMode(1); // byteCount
369 - } else if (isWriteResponse(fc)) { 367 + } else if (IotModbusUtils.isWriteResponse(fc)) {
370 // 写响应 368 // 写响应
371 state = STATE_WRITE_BODY; 369 state = STATE_WRITE_BODY;
372 pendingData = Buffer.buffer(); 370 pendingData = Buffer.buffer();
@@ -438,14 +436,6 @@ public class IotModbusFrameDecoder { @@ -438,14 +436,6 @@ public class IotModbusFrameDecoder {
438 parser.fixedSizeMode(2); // slaveId + FC 436 parser.fixedSizeMode(2); // slaveId + FC
439 } 437 }
440 438
441 - // TODO @AI:可以抽到 IotModbusUtils 里?  
442 - private boolean isReadResponse(int fc) {  
443 - return fc >= 1 && fc <= 4;  
444 - }  
445 -  
446 - private boolean isWriteResponse(int fc) {  
447 - return fc == 5 || fc == 6 || fc == 15 || fc == 16;  
448 - }  
449 } 439 }
450 440
451 } 441 }
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusFrameEncoder.java
1 package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec; 1 package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec;
2 2
3 import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; 3 import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
  4 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils;
4 import lombok.RequiredArgsConstructor; 5 import lombok.RequiredArgsConstructor;
5 import lombok.extern.slf4j.Slf4j; 6 import lombok.extern.slf4j.Slf4j;
6 7
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/codec/IotModbusUtils.java deleted
1 -package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec;  
2 -  
3 -import lombok.extern.slf4j.Slf4j;  
4 -  
5 -/**  
6 - * IoT Modbus 工具类  
7 - * <p>  
8 - * 提供:  
9 - * 1. CRC-16/MODBUS 计算和校验  
10 - * 2. 从解码后的 IotModbusFrame 中提取寄存器值(用于后续的点位翻译)  
11 - *  
12 - * @author 芋道源码  
13 - */  
14 -@Slf4j  
15 -public class IotModbusUtils {  
16 -  
17 - // TODO @AI:可以把 1、2、3、4、5 这些 fucntion code 在这里枚举下。  
18 - // TODO @AI:一些枚举 0x80 这些可以这里枚举;  
19 -  
20 - // ==================== CRC16 工具 ====================  
21 -  
22 - /**  
23 - * 计算 CRC-16/MODBUS  
24 - *  
25 - * @param data 数据  
26 - * @param length 计算长度  
27 - * @return CRC16 值  
28 - */  
29 - public static int calculateCrc16(byte[] data, int length) {  
30 - int crc = 0xFFFF;  
31 - for (int i = 0; i < length; i++) {  
32 - crc ^= (data[i] & 0xFF);  
33 - for (int j = 0; j < 8; j++) {  
34 - if ((crc & 0x0001) != 0) {  
35 - crc >>= 1;  
36 - crc ^= 0xA001;  
37 - } else {  
38 - crc >>= 1;  
39 - }  
40 - }  
41 - }  
42 - return crc;  
43 - }  
44 -  
45 - /**  
46 - * 校验 CRC16  
47 - *  
48 - * @param data 包含 CRC 的完整数据  
49 - * @return 校验是否通过  
50 - */  
51 - public static boolean verifyCrc16(byte[] data) {  
52 - if (data.length < 3) {  
53 - return false;  
54 - }  
55 - int computed = calculateCrc16(data, data.length - 2);  
56 - int received = (data[data.length - 2] & 0xFF) | ((data[data.length - 1] & 0xFF) << 8);  
57 - return computed == received;  
58 - }  
59 -  
60 - // ==================== 响应值提取 ====================  
61 -  
62 - /**  
63 - * 从帧中提取寄存器值(FC01-04 读响应)  
64 - *  
65 - * @param frame 解码后的 Modbus 帧  
66 - * @return 寄存器值数组(int[]),失败返回 null  
67 - */  
68 - @SuppressWarnings("EnhancedSwitchMigration")  
69 - public static int[] extractValues(IotModbusFrame frame) {  
70 - if (frame == null || frame.isException()) {  
71 - return null;  
72 - }  
73 - byte[] pdu = frame.getPdu();  
74 - if (pdu == null || pdu.length < 1) {  
75 - return null;  
76 - }  
77 -  
78 - // TODO @AI:jmodbus 看看有没可以复用的枚举类  
79 - int functionCode = frame.getFunctionCode();  
80 - switch (functionCode) {  
81 - case 1: // Read Coils  
82 - case 2: // Read Discrete Inputs  
83 - return extractCoilValues(pdu);  
84 - case 3: // Read Holding Registers  
85 - case 4: // Read Input Registers  
86 - return extractRegisterValues(pdu);  
87 - default:  
88 - log.warn("[extractValues][不支持的功能码: {}]", functionCode);  
89 - return null;  
90 - }  
91 - }  
92 -  
93 - /**  
94 - * 提取线圈/离散输入值  
95 - * PDU 格式(FC01/02 响应):[ByteCount(1)] [CoilStatus(N)]  
96 - */  
97 - private static int[] extractCoilValues(byte[] pdu) {  
98 - if (pdu.length < 2) {  
99 - return null;  
100 - }  
101 - int byteCount = pdu[0] & 0xFF;  
102 - int bitCount = byteCount * 8;  
103 - int[] values = new int[bitCount];  
104 - for (int i = 0; i < bitCount && (1 + i / 8) < pdu.length; i++) {  
105 - values[i] = ((pdu[1 + i / 8] >> (i % 8)) & 0x01);  
106 - }  
107 - return values;  
108 - }  
109 -  
110 - /**  
111 - * 提取寄存器值  
112 - * PDU 格式(FC03/04 响应):[ByteCount(1)] [RegisterData(N*2)]  
113 - */  
114 - private static int[] extractRegisterValues(byte[] pdu) {  
115 - if (pdu.length < 2) {  
116 - return null;  
117 - }  
118 - int byteCount = pdu[0] & 0xFF;  
119 - int registerCount = byteCount / 2;  
120 - int[] values = new int[registerCount];  
121 - for (int i = 0; i < registerCount && (1 + i * 2 + 1) < pdu.length; i++) {  
122 - values[i] = ((pdu[1 + i * 2] & 0xFF) << 8) | (pdu[1 + i * 2 + 1] & 0xFF);  
123 - }  
124 - return values;  
125 - }  
126 -  
127 -}  
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamHandler.java
1 package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream; 1 package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream;
2 2
3 -import cn.hutool.core.collection.CollUtil; 3 +import cn.hutool.core.lang.Assert;
  4 +import cn.hutool.core.util.ObjUtil;
4 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; 5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO; 6 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
6 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 7 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
7 import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; 8 import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
8 -import cn.iocoder.yudao.module.iot.core.enums.IotModbusFunctionCodeEnum;  
9 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 9 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
10 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; 10 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter;
  11 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils;
11 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; 12 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder;
12 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService; 13 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService;
13 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager; 14 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager;
@@ -18,7 +19,6 @@ import lombok.extern.slf4j.Slf4j; @@ -18,7 +19,6 @@ import lombok.extern.slf4j.Slf4j;
18 import java.util.Map; 19 import java.util.Map;
19 import java.util.concurrent.atomic.AtomicInteger; 20 import java.util.concurrent.atomic.AtomicInteger;
20 21
21 -// TODO @AI:看看能不能和 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpmaster/handler/downstream/IotModbusTcpDownstreamHandler.java 有一些复用逻辑;  
22 /** 22 /**
23 * IoT Modbus TCP Slave 下行消息处理器 23 * IoT Modbus TCP Slave 下行消息处理器
24 * <p> 24 * <p>
@@ -48,7 +48,7 @@ public class IotModbusTcpSlaveDownstreamHandler { @@ -48,7 +48,7 @@ public class IotModbusTcpSlaveDownstreamHandler {
48 @SuppressWarnings("unchecked") 48 @SuppressWarnings("unchecked")
49 public void handle(IotDeviceMessage message) { 49 public void handle(IotDeviceMessage message) {
50 // 1.1 检查是否是属性设置消息 50 // 1.1 检查是否是属性设置消息
51 - if (!IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod().equals(message.getMethod())) { 51 + if (ObjUtil.notEqual(IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod(), message.getMethod())) {
52 log.debug("[handle][忽略非属性设置消息: {}]", message.getMethod()); 52 log.debug("[handle][忽略非属性设置消息: {}]", message.getMethod());
53 return; 53 return;
54 } 54 }
@@ -76,13 +76,13 @@ public class IotModbusTcpSlaveDownstreamHandler { @@ -76,13 +76,13 @@ public class IotModbusTcpSlaveDownstreamHandler {
76 String identifier = entry.getKey(); 76 String identifier = entry.getKey();
77 Object value = entry.getValue(); 77 Object value = entry.getValue();
78 // 2.1 查找对应的点位配置 78 // 2.1 查找对应的点位配置
79 - IotModbusPointRespDTO point = findPoint(config, identifier); 79 + IotModbusPointRespDTO point = IotModbusUtils.findPoint(config, identifier);
80 if (point == null) { 80 if (point == null) {
81 log.warn("[handle][设备 {} 没有点位配置: {}]", message.getDeviceId(), identifier); 81 log.warn("[handle][设备 {} 没有点位配置: {}]", message.getDeviceId(), identifier);
82 continue; 82 continue;
83 } 83 }
84 // 2.2 检查是否支持写操作 84 // 2.2 检查是否支持写操作
85 - if (!isWritable(point.getFunctionCode())) { 85 + if (!IotModbusUtils.isWritable(point.getFunctionCode())) {
86 log.warn("[handle][点位 {} 不支持写操作, 功能码={}]", identifier, point.getFunctionCode()); 86 log.warn("[handle][点位 {} 不支持写操作, 功能码={}]", identifier, point.getFunctionCode());
87 continue; 87 continue;
88 } 88 }
@@ -97,56 +97,36 @@ public class IotModbusTcpSlaveDownstreamHandler { @@ -97,56 +97,36 @@ public class IotModbusTcpSlaveDownstreamHandler {
97 */ 97 */
98 private void writeProperty(Long deviceId, ConnectionInfo connInfo, 98 private void writeProperty(Long deviceId, ConnectionInfo connInfo,
99 IotModbusPointRespDTO point, Object value) { 99 IotModbusPointRespDTO point, Object value) {
100 - // 1. 转换属性值为原始值 100 + // 1.1 转换属性值为原始值
101 int[] rawValues = dataConverter.convertToRawValues(value, point); 101 int[] rawValues = dataConverter.convertToRawValues(value, point);
102 102
103 - // 2. 确定帧格式和事务 ID 103 + // 1.2 确定帧格式和事务 ID
104 IotModbusFrameFormatEnum frameFormat = connInfo.getFrameFormat(); 104 IotModbusFrameFormatEnum frameFormat = connInfo.getFrameFormat();
105 - if (frameFormat == null) {  
106 - frameFormat = IotModbusFrameFormatEnum.MODBUS_TCP;  
107 - } 105 + Assert.notNull(frameFormat, "连接帧格式不能为空");
108 int transactionId = transactionIdCounter.incrementAndGet() & 0xFFFF; 106 int transactionId = transactionIdCounter.incrementAndGet() & 0xFFFF;
109 int slaveId = connInfo.getSlaveId() != null ? connInfo.getSlaveId() : 1; 107 int slaveId = connInfo.getSlaveId() != null ? connInfo.getSlaveId() : 1;
110 -  
111 - // 3. 编码写请求 108 + // 1.3 编码写请求
112 byte[] data; 109 byte[] data;
113 - IotModbusFunctionCodeEnum fcEnum = IotModbusFunctionCodeEnum.valueOf(point.getFunctionCode());  
114 - if (fcEnum == null) {  
115 - log.warn("[writeProperty][未知功能码: {}]", point.getFunctionCode());  
116 - return;  
117 - }  
118 - if (rawValues.length == 1 && fcEnum.getWriteSingleCode() != null) { 110 + int readFunctionCode = point.getFunctionCode();
  111 + Integer writeSingleCode = IotModbusUtils.getWriteSingleFunctionCode(readFunctionCode);
  112 + Integer writeMultipleCode = IotModbusUtils.getWriteMultipleFunctionCode(readFunctionCode);
  113 + if (rawValues.length == 1 && writeSingleCode != null) {
119 // 单个值:使用单写功能码(FC05/FC06) 114 // 单个值:使用单写功能码(FC05/FC06)
120 - data = frameEncoder.encodeWriteSingleRequest(slaveId, fcEnum.getWriteSingleCode(), 115 + data = frameEncoder.encodeWriteSingleRequest(slaveId, writeSingleCode,
121 point.getRegisterAddress(), rawValues[0], frameFormat, transactionId); 116 point.getRegisterAddress(), rawValues[0], frameFormat, transactionId);
122 - } else if (fcEnum.getWriteMultipleCode() != null) { 117 + } else if (writeMultipleCode != null) {
123 // 多个值:使用多写功能码(FC15/FC16) 118 // 多个值:使用多写功能码(FC15/FC16)
124 data = frameEncoder.encodeWriteMultipleRegistersRequest(slaveId, 119 data = frameEncoder.encodeWriteMultipleRegistersRequest(slaveId,
125 point.getRegisterAddress(), rawValues, frameFormat, transactionId); 120 point.getRegisterAddress(), rawValues, frameFormat, transactionId);
126 } else { 121 } else {
127 - log.warn("[writeProperty][点位 {} 不支持写操作]", point.getIdentifier()); 122 + log.warn("[writeProperty][点位 {} 不支持写操作, 功能码={}]", point.getIdentifier(), readFunctionCode);
128 return; 123 return;
129 } 124 }
130 125
131 - // 4. 发送 126 + // 2. 发送
132 connectionManager.sendToDevice(deviceId, data); 127 connectionManager.sendToDevice(deviceId, data);
133 log.info("[writeProperty][写入成功, deviceId={}, identifier={}, value={}]", 128 log.info("[writeProperty][写入成功, deviceId={}, identifier={}, value={}]",
134 deviceId, point.getIdentifier(), value); 129 deviceId, point.getIdentifier(), value);
135 } 130 }
136 131
137 - /**  
138 - * 查找点位配置  
139 - */  
140 - private IotModbusPointRespDTO findPoint(IotModbusDeviceConfigRespDTO config, String identifier) {  
141 - return CollUtil.findOne(config.getPoints(), p -> identifier.equals(p.getIdentifier()));  
142 - }  
143 -  
144 - /**  
145 - * 检查功能码是否支持写操作  
146 - */  
147 - private boolean isWritable(Integer functionCode) {  
148 - IotModbusFunctionCodeEnum functionCodeEnum = IotModbusFunctionCodeEnum.valueOf(functionCode);  
149 - return functionCodeEnum != null && Boolean.TRUE.equals(functionCodeEnum.getWritable());  
150 - }  
151 -  
152 } 132 }
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/downstream/IotModbusTcpSlaveDownstreamSubscriber.java
1 package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream; 1 package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.handler.downstream;
2 2
3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; 3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4 -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;  
5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
6 -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; 5 +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
7 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.IotModbusTcpSlaveProtocol; 6 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.IotModbusTcpSlaveProtocol;
8 -import lombok.RequiredArgsConstructor;  
9 import lombok.extern.slf4j.Slf4j; 7 import lombok.extern.slf4j.Slf4j;
10 8
11 -// TODO @AI:是不是可以继承 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolDownstreamSubscriber.java  
12 /** 9 /**
13 * IoT Modbus TCP Slave 下行消息订阅器:订阅消息总线的下行消息并转发给处理器 10 * IoT Modbus TCP Slave 下行消息订阅器:订阅消息总线的下行消息并转发给处理器
14 * 11 *
15 * @author 芋道源码 12 * @author 芋道源码
16 */ 13 */
17 -@RequiredArgsConstructor  
18 @Slf4j 14 @Slf4j
19 -public class IotModbusTcpSlaveDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> { 15 +public class IotModbusTcpSlaveDownstreamSubscriber extends IotProtocolDownstreamSubscriber {
20 16
21 - private final IotModbusTcpSlaveProtocol protocol;  
22 private final IotModbusTcpSlaveDownstreamHandler downstreamHandler; 17 private final IotModbusTcpSlaveDownstreamHandler downstreamHandler;
23 - private final IotMessageBus messageBus;  
24 18
25 - /**  
26 - * 启动订阅  
27 - */  
28 - public void start() {  
29 - messageBus.register(this);  
30 - log.info("[start][Modbus TCP Slave 下行消息订阅器已启动, topic={}]", getTopic());  
31 - }  
32 -  
33 - /**  
34 - * 停止订阅  
35 - */  
36 - public void stop() {  
37 - messageBus.unregister(this);  
38 - log.info("[stop][Modbus TCP Slave 下行消息订阅器已停止]");  
39 - }  
40 -  
41 - @Override  
42 - public String getTopic() {  
43 - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId());  
44 - }  
45 -  
46 - @Override  
47 - public String getGroup() {  
48 - return getTopic(); // 点对点消费 19 + public IotModbusTcpSlaveDownstreamSubscriber(IotModbusTcpSlaveProtocol protocol,
  20 + IotModbusTcpSlaveDownstreamHandler downstreamHandler,
  21 + IotMessageBus messageBus) {
  22 + super(protocol, messageBus);
  23 + this.downstreamHandler = downstreamHandler;
49 } 24 }
50 25
51 @Override 26 @Override
52 - public void onMessage(IotDeviceMessage message) {  
53 - log.debug("[onMessage][收到下行消息: {}]", message);  
54 - try {  
55 - downstreamHandler.handle(message);  
56 - } catch (Exception e) {  
57 - log.error("[onMessage][处理下行消息失败]", e);  
58 - } 27 + protected void handleMessage(IotDeviceMessage message) {
  28 + downstreamHandler.handle(message);
59 } 29 }
60 30
61 } 31 }
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/handler/upstream/IotModbusTcpSlaveUpstreamHandler.java
@@ -5,14 +5,15 @@ import cn.hutool.core.lang.Assert; @@ -5,14 +5,15 @@ import cn.hutool.core.lang.Assert;
5 import cn.hutool.core.map.MapUtil; 5 import cn.hutool.core.map.MapUtil;
6 import cn.hutool.core.util.BooleanUtil; 6 import cn.hutool.core.util.BooleanUtil;
7 import cn.hutool.core.util.StrUtil; 7 import cn.hutool.core.util.StrUtil;
8 -import cn.hutool.json.JSONObject;  
9 -import cn.hutool.json.JSONUtil;  
10 import cn.iocoder.yudao.framework.common.exception.ServiceException; 8 import cn.iocoder.yudao.framework.common.exception.ServiceException;
  9 +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
11 import cn.iocoder.yudao.framework.common.pojo.CommonResult; 10 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  11 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
12 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; 12 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
13 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; 13 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
14 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; 14 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
15 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO; 15 import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
  16 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
16 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 17 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
17 import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum; 18 import cn.iocoder.yudao.module.iot.core.enums.IotModbusFrameFormatEnum;
18 import cn.iocoder.yudao.module.iot.core.enums.IotModbusModeEnum; 19 import cn.iocoder.yudao.module.iot.core.enums.IotModbusModeEnum;
@@ -20,9 +21,9 @@ import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; @@ -20,9 +21,9 @@ import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
20 import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity; 21 import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
21 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; 22 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
22 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter; 23 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusDataConverter;
  24 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils;
23 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; 25 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame;
24 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; 26 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder;
25 -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusUtils;  
26 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService; 27 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConfigCacheService;
27 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager; 28 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager;
28 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo; 29 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.manager.IotModbusTcpSlaveConnectionManager.ConnectionInfo;
@@ -40,14 +41,12 @@ import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeC @@ -40,14 +41,12 @@ import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeC
40 import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; 41 import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
41 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; 42 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
42 43
43 -// DONE @AI:逻辑有点多,看看是不是分区域! => 已按区域划分:认证 / 轮询响应  
44 /** 44 /**
45 * IoT Modbus TCP Slave 上行数据处理器 45 * IoT Modbus TCP Slave 上行数据处理器
46 * <p> 46 * <p>
47 * 处理: 47 * 处理:
48 * 1. 自定义 FC 认证 48 * 1. 自定义 FC 认证
49 - * 2. 轮询响应(mode=1)→ 点位翻译 → thing.property.post  
50 - * // DONE @AI:不用主动上报;主动上报走标准 tcp 即可 49 + * 2. 轮询响应 → 点位翻译 → thing.property.post
51 * 50 *
52 * @author 芋道源码 51 * @author 芋道源码
53 */ 52 */
@@ -105,29 +104,14 @@ public class IotModbusTcpSlaveUpstreamHandler { @@ -105,29 +104,14 @@ public class IotModbusTcpSlaveUpstreamHandler {
105 return; 104 return;
106 } 105 }
107 106
108 - // 2. 自定义功能码(认证等扩展) 107 + // 2. 情况一:自定义功能码(认证等扩展)
109 if (StrUtil.isNotEmpty(frame.getCustomData())) { 108 if (StrUtil.isNotEmpty(frame.getCustomData())) {
110 handleCustomFrame(socket, frame, frameFormat); 109 handleCustomFrame(socket, frame, frameFormat);
111 return; 110 return;
112 } 111 }
113 112
114 - // 1.2 未认证连接,丢弃  
115 - // TODO @AI:把 1.2、1.3 拿到 handlePollingResponse 里;是否需要登录,自己知道!  
116 - if (!connectionManager.isAuthenticated(socket)) {  
117 - log.warn("[handleFrame][未认证连接, 丢弃数据, remoteAddress={}]", socket.remoteAddress());  
118 - return;  
119 - }  
120 -  
121 - // 3. DONE @AI:断言必须是云端轮询(不再支持主动上报)  
122 - // TODO @AI:貌似只能轮询到一次?!  
123 - // 1.3 标准 Modbus 响应(只支持云端轮询模式)  
124 - // TODO @AI:可以把  
125 - ConnectionInfo info = connectionManager.getConnectionInfo(socket);  
126 - if (info == null) {  
127 - log.warn("[handleFrame][已认证但连接信息为空, remoteAddress={}]", socket.remoteAddress());  
128 - return;  
129 - }  
130 - handlePollingResponse(info, frame, frameFormat); 113 + // 3. 情况二:标准 Modbus 响应 → 轮询响应处理
  114 + handlePollingResponse(socket, frame, frameFormat);
131 } 115 }
132 116
133 // ========== 自定义 FC 处理(认证等) ========== 117 // ========== 自定义 FC 处理(认证等) ==========
@@ -138,96 +122,83 @@ public class IotModbusTcpSlaveUpstreamHandler { @@ -138,96 +122,83 @@ public class IotModbusTcpSlaveUpstreamHandler {
138 * 异常分层翻译,参考 {@link cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpAbstractHandler} 122 * 异常分层翻译,参考 {@link cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpAbstractHandler}
139 */ 123 */
140 private void handleCustomFrame(NetSocket socket, IotModbusFrame frame, IotModbusFrameFormatEnum frameFormat) { 124 private void handleCustomFrame(NetSocket socket, IotModbusFrame frame, IotModbusFrameFormatEnum frameFormat) {
  125 + String method = null;
141 try { 126 try {
142 - JSONObject json = JSONUtil.parseObj(frame.getCustomData());  
143 - String method = json.getStr("method"); 127 + IotDeviceMessage message = JsonUtils.parseObject(frame.getCustomData(), IotDeviceMessage.class);
  128 + if (message == null) {
  129 + throw invalidParamException("自定义 FC 数据解析失败");
  130 + }
  131 + method = message.getMethod();
144 if (METHOD_AUTH.equals(method)) { 132 if (METHOD_AUTH.equals(method)) {
145 - handleAuth(socket, frame, json, frameFormat); 133 + handleAuth(socket, frame, frameFormat, message.getParams());
146 return; 134 return;
147 } 135 }
148 log.warn("[handleCustomFrame][未知 method: {}, frame: slaveId={}, FC={}, customData={}]", 136 log.warn("[handleCustomFrame][未知 method: {}, frame: slaveId={}, FC={}, customData={}]",
149 method, frame.getSlaveId(), frame.getFunctionCode(), frame.getCustomData()); 137 method, frame.getSlaveId(), frame.getFunctionCode(), frame.getCustomData());
150 } catch (ServiceException e) { 138 } catch (ServiceException e) {
151 // 已知业务异常,返回对应的错误码和错误信息 139 // 已知业务异常,返回对应的错误码和错误信息
152 - sendCustomResponse(socket, frame, frameFormat, e.getCode(), e.getMessage()); 140 + sendCustomResponse(socket, frame, frameFormat, method, e.getCode(), e.getMessage());
153 } catch (IllegalArgumentException e) { 141 } catch (IllegalArgumentException e) {
154 // 参数校验异常,返回 400 错误 142 // 参数校验异常,返回 400 错误
155 - sendCustomResponse(socket, frame, frameFormat, BAD_REQUEST.getCode(), e.getMessage()); 143 + sendCustomResponse(socket, frame, frameFormat, method, BAD_REQUEST.getCode(), e.getMessage());
156 } catch (Exception e) { 144 } catch (Exception e) {
157 // 其他未知异常,返回 500 错误 145 // 其他未知异常,返回 500 错误
158 log.error("[handleCustomFrame][解析自定义 FC 数据失败, frame: slaveId={}, FC={}, customData={}]", 146 log.error("[handleCustomFrame][解析自定义 FC 数据失败, frame: slaveId={}, FC={}, customData={}]",
159 frame.getSlaveId(), frame.getFunctionCode(), frame.getCustomData(), e); 147 frame.getSlaveId(), frame.getFunctionCode(), frame.getCustomData(), e);
160 - sendCustomResponse(socket, frame, frameFormat, 148 + sendCustomResponse(socket, frame, frameFormat, method,
161 INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); 149 INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
162 } 150 }
163 } 151 }
164 152
165 - // TODO @芋艿:在 review 下这个类;  
166 - // TODO @AI:不传递 json,直接在 frame  
167 /** 153 /**
168 * 处理认证请求 154 * 处理认证请求
169 */ 155 */
170 - private void handleAuth(NetSocket socket, IotModbusFrame frame, JSONObject json,  
171 - IotModbusFrameFormatEnum frameFormat) {  
172 - // TODO @AI:是不是可以 JsonUtils.convert(json, IotDeviceAuthReqDTO.class);  
173 - JSONObject params = json.getJSONObject("params");  
174 - if (params == null) {  
175 - throw invalidParamException("params 不能为空");  
176 - }  
177 - // DONE @AI:参数判空  
178 - String clientId = params.getStr("clientId");  
179 - String username = params.getStr("username");  
180 - String password = params.getStr("password");  
181 - // TODO @AI:逐个判空;  
182 - if (StrUtil.hasBlank(clientId, username, password)) {  
183 - throw invalidParamException("clientId、username、password 不能为空");  
184 - } 156 + @SuppressWarnings("DataFlowIssue")
  157 + private void handleAuth(NetSocket socket, IotModbusFrame frame, IotModbusFrameFormatEnum frameFormat, Object params) {
  158 + // 1. 解析认证参数
  159 + IotDeviceAuthReqDTO request = JsonUtils.convertObject(params, IotDeviceAuthReqDTO.class);
  160 + Assert.notNull(request, "认证参数不能为空");
  161 + Assert.notBlank(request.getClientId(), "clientId 不能为空");
  162 + Assert.notBlank(request.getUsername(), "username 不能为空");
  163 + Assert.notBlank(request.getPassword(), "password 不能为空");
185 164
186 - // 1. 调用认证 API  
187 - IotDeviceAuthReqDTO authReq = new IotDeviceAuthReqDTO()  
188 - .setClientId(clientId).setUsername(username).setPassword(password);  
189 - CommonResult<Boolean> authResult = deviceApi.authDevice(authReq);  
190 - authResult.checkError();  
191 - if (BooleanUtil.isFalse(authResult.getData())) {  
192 - log.warn("[handleAuth][认证失败, clientId={}, username={}]", clientId, username);  
193 - sendCustomResponse(socket, frame, frameFormat, 1, "认证失败"); 165 + // 2.1 调用认证 API
  166 + CommonResult<Boolean> result = deviceApi.authDevice(request);
  167 + result.checkError();
  168 + if (BooleanUtil.isFalse(result.getData())) {
  169 + log.warn("[handleAuth][认证失败, clientId={}, username={}]", request.getClientId(), request.getUsername());
  170 + sendCustomResponse(socket, frame, frameFormat, METHOD_AUTH, 1, "认证失败");
194 return; 171 return;
195 } 172 }
196 -  
197 - // 2.1 认证成功,查找设备配置  
198 - IotModbusDeviceConfigRespDTO config = configCacheService.findConfigByAuth(clientId, username, password);  
199 - if (config == null) {  
200 - log.info("[handleAuth][认证成功但未找到设备配置, clientId={}, username={}]", clientId, username);  
201 - }  
202 // 2.2 解析设备信息 173 // 2.2 解析设备信息
203 - IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username); 174 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(request.getUsername());
204 Assert.notNull(deviceInfo, "解析设备信息失败"); 175 Assert.notNull(deviceInfo, "解析设备信息失败");
205 // 2.3 获取设备信息 176 // 2.3 获取设备信息
206 - // DONE @AI:IotDeviceService 作为构造参数传入,不通过 SpringUtil.getBean  
207 IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); 177 IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName());
208 Assert.notNull(device, "设备不存在"); 178 Assert.notNull(device, "设备不存在");
209 179
210 - // 3. 注册连接 180 + // 3.1 注册连接
211 ConnectionInfo connectionInfo = new ConnectionInfo() 181 ConnectionInfo connectionInfo = new ConnectionInfo()
212 .setDeviceId(device.getId()) 182 .setDeviceId(device.getId())
  183 + .setProductKey(deviceInfo.getProductKey())
  184 + .setDeviceName(deviceInfo.getDeviceName())
213 .setSlaveId(frame.getSlaveId()) 185 .setSlaveId(frame.getSlaveId())
214 - .setFrameFormat(frameFormat)  
215 - .setMode(config != null ? config.getMode() : IotModbusModeEnum.POLLING.getMode());  
216 - if (config != null) {  
217 - connectionInfo.setDeviceId(config.getDeviceId())  
218 - .setProductKey(config.getProductKey())  
219 - .setDeviceName(config.getDeviceName());  
220 - } 186 + .setFrameFormat(frameFormat);
221 connectionManager.registerConnection(socket, connectionInfo); 187 connectionManager.registerConnection(socket, connectionInfo);
  188 + // 3.2 发送上线消息
  189 + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline();
  190 + messageService.sendDeviceMessage(onlineMessage, deviceInfo.getProductKey(), deviceInfo.getDeviceName(), serverId);
  191 + // 3.3 发送成功响应
  192 + sendCustomResponse(socket, frame, frameFormat, METHOD_AUTH,
  193 + GlobalErrorCodeConstants.SUCCESS.getCode(), "success");
  194 + log.info("[handleAuth][认证成功, clientId={}, deviceId={}]", request.getClientId(), device.getId());
222 195
223 - // 4. 发送认证成功响应  
224 - sendCustomResponse(socket, frame, frameFormat, 0, "success");  
225 - log.info("[handleAuth][认证成功, clientId={}, deviceId={}]", clientId,  
226 - config != null ? config.getDeviceId() : device.getId());  
227 -  
228 - // 5. 直接启动轮询 196 + // 4. 加载设备配置并启动轮询
  197 + IotModbusDeviceConfigRespDTO config = configCacheService.loadDeviceConfig(device.getId());
229 if (config != null) { 198 if (config != null) {
230 pollScheduler.updatePolling(config); 199 pollScheduler.updatePolling(config);
  200 + } else {
  201 + log.warn("[handleAuth][认证成功但未找到设备配置, deviceId={}]", device.getId());
231 } 202 }
232 } 203 }
233 204
@@ -236,12 +207,13 @@ public class IotModbusTcpSlaveUpstreamHandler { @@ -236,12 +207,13 @@ public class IotModbusTcpSlaveUpstreamHandler {
236 */ 207 */
237 private void sendCustomResponse(NetSocket socket, IotModbusFrame frame, 208 private void sendCustomResponse(NetSocket socket, IotModbusFrame frame,
238 IotModbusFrameFormatEnum frameFormat, 209 IotModbusFrameFormatEnum frameFormat,
239 - int code, String message) {  
240 - JSONObject resp = new JSONObject();  
241 - resp.set("method", METHOD_AUTH);  
242 - resp.set("code", code);  
243 - resp.set("message", message);  
244 - byte[] data = frameEncoder.encodeCustomFrame(frame.getSlaveId(), resp.toString(), 210 + String method, int code, String message) {
  211 + Map<String, Object> response = MapUtil.<String, Object>builder()
  212 + .put("method", method)
  213 + .put("code", code)
  214 + .put("message", message)
  215 + .build();
  216 + byte[] data = frameEncoder.encodeCustomFrame(frame.getSlaveId(), JsonUtils.toJsonString(response),
245 frameFormat, frame.getTransactionId() != null ? frame.getTransactionId() : 0); 217 frameFormat, frame.getTransactionId() != null ? frame.getTransactionId() : 0);
246 connectionManager.sendToSocket(socket, data); 218 connectionManager.sendToSocket(socket, data);
247 } 219 }
@@ -251,9 +223,16 @@ public class IotModbusTcpSlaveUpstreamHandler { @@ -251,9 +223,16 @@ public class IotModbusTcpSlaveUpstreamHandler {
251 /** 223 /**
252 * 处理轮询响应(云端轮询模式) 224 * 处理轮询响应(云端轮询模式)
253 */ 225 */
254 - private void handlePollingResponse(ConnectionInfo info, IotModbusFrame frame, 226 + private void handlePollingResponse(NetSocket socket, IotModbusFrame frame,
255 IotModbusFrameFormatEnum frameFormat) { 227 IotModbusFrameFormatEnum frameFormat) {
256 - // 1.1 匹配 PendingRequest 228 + // 1. 获取连接信息(未认证连接丢弃)
  229 + ConnectionInfo info = connectionManager.getConnectionInfo(socket);
  230 + if (info == null) {
  231 + log.warn("[handlePollingResponse][未认证连接, 丢弃数据, remoteAddress={}]", socket.remoteAddress());
  232 + return;
  233 + }
  234 +
  235 + // 2.1 匹配 PendingRequest
257 PendingRequest request = pendingRequestManager.matchResponse( 236 PendingRequest request = pendingRequestManager.matchResponse(
258 info.getDeviceId(), frame, frameFormat); 237 info.getDeviceId(), frame, frameFormat);
259 if (request == null) { 238 if (request == null) {
@@ -261,26 +240,27 @@ public class IotModbusTcpSlaveUpstreamHandler { @@ -261,26 +240,27 @@ public class IotModbusTcpSlaveUpstreamHandler {
261 info.getDeviceId(), frame.getFunctionCode()); 240 info.getDeviceId(), frame.getFunctionCode());
262 return; 241 return;
263 } 242 }
264 - // 1.2 提取寄存器值 243 + // 2.2 提取寄存器值
265 int[] rawValues = IotModbusUtils.extractValues(frame); 244 int[] rawValues = IotModbusUtils.extractValues(frame);
266 if (rawValues == null) { 245 if (rawValues == null) {
267 log.warn("[handlePollingResponse][提取寄存器值失败, deviceId={}, identifier={}]", 246 log.warn("[handlePollingResponse][提取寄存器值失败, deviceId={}, identifier={}]",
268 info.getDeviceId(), request.getIdentifier()); 247 info.getDeviceId(), request.getIdentifier());
269 return; 248 return;
270 } 249 }
271 - // 1.3 查找点位配置 250 + // 2.3 查找点位配置
272 IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(info.getDeviceId()); 251 IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(info.getDeviceId());
273 if (config == null || CollUtil.isEmpty(config.getPoints())) { 252 if (config == null || CollUtil.isEmpty(config.getPoints())) {
274 return; 253 return;
275 } 254 }
276 - var point = CollUtil.findOne(config.getPoints(), p -> p.getId().equals(request.getPointId())); 255 + IotModbusPointRespDTO point = CollUtil.findOne(config.getPoints(),
  256 + p -> p.getId().equals(request.getPointId()));
277 if (point == null) { 257 if (point == null) {
278 return; 258 return;
279 } 259 }
280 260
281 - // 2.1 点位翻译 261 + // 3.1 点位翻译
282 Object convertedValue = dataConverter.convertToPropertyValue(rawValues, point); 262 Object convertedValue = dataConverter.convertToPropertyValue(rawValues, point);
283 - // 2.2 上报属性 263 + // 3.2 上报属性
284 Map<String, Object> params = MapUtil.of(request.getIdentifier(), convertedValue); 264 Map<String, Object> params = MapUtil.of(request.getIdentifier(), convertedValue);
285 IotDeviceMessage message = IotDeviceMessage.requestOf( 265 IotDeviceMessage message = IotDeviceMessage.requestOf(
286 IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), params); 266 IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), params);
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/manager/IotModbusTcpSlaveConfigCacheService.java
@@ -80,7 +80,7 @@ public class IotModbusTcpSlaveConfigCacheService { @@ -80,7 +80,7 @@ public class IotModbusTcpSlaveConfigCacheService {
80 */ 80 */
81 private List<IotModbusDeviceConfigRespDTO> buildMockConfigs() { 81 private List<IotModbusDeviceConfigRespDTO> buildMockConfigs() {
82 IotModbusDeviceConfigRespDTO config = new IotModbusDeviceConfigRespDTO(); 82 IotModbusDeviceConfigRespDTO config = new IotModbusDeviceConfigRespDTO();
83 - config.setDeviceId(1L); 83 + config.setDeviceId(25L);
84 config.setProductKey("4aymZgOTOOCrDKRT"); 84 config.setProductKey("4aymZgOTOOCrDKRT");
85 config.setDeviceName("small"); 85 config.setDeviceName("small");
86 config.setSlaveId(1); 86 config.setSlaveId(1);
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpslave/IotModbusTcpSlaveModbusRtuIntegrationTest.java
@@ -8,7 +8,7 @@ import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; @@ -8,7 +8,7 @@ import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
8 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame; 8 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrame;
9 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameDecoder; 9 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameDecoder;
10 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder; 10 import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusFrameEncoder;
11 -import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpslave.codec.IotModbusUtils; 11 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.IotModbusUtils;
12 import io.vertx.core.Vertx; 12 import io.vertx.core.Vertx;
13 import io.vertx.core.buffer.Buffer; 13 import io.vertx.core.buffer.Buffer;
14 import io.vertx.core.net.NetClient; 14 import io.vertx.core.net.NetClient;