Commit bf3b19d3fb9969f300caefe946e6925c6a095f16

Authored by 芋道源码
Committed by Gitee
2 parents ff2618bb cc9cca82

!1498 feat:【iot】modbus-tcp 协议接入:100%

Merge pull request !1498 from 芋道源码/feature/iot-modbus
Showing 133 changed files with 7297 additions and 269 deletions
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java
1 1 package cn.iocoder.yudao.module.iot.api.device;
2 2  
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.ObjUtil;
  5 +import cn.hutool.core.util.StrUtil;
3 6 import cn.iocoder.yudao.framework.common.enums.RpcConstants;
4 7 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
5 8 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  9 +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
6 10 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
7   -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
8   -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
9   -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
10   -import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
  11 +import cn.iocoder.yudao.module.iot.core.biz.dto.*;
11 12 import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
12 13 import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
13 14 import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
14 15 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
  16 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO;
  17 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusPointDO;
15 18 import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
  19 +import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusConfigService;
  20 +import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusPointService;
16 21 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
17 22 import cn.iocoder.yudao.module.iot.service.product.IotProductService;
18 23 import jakarta.annotation.Resource;
19 24 import jakarta.annotation.security.PermitAll;
  25 +import org.springframework.context.annotation.Lazy;
20 26 import org.springframework.context.annotation.Primary;
21 27 import org.springframework.validation.annotation.Validated;
22 28 import org.springframework.web.bind.annotation.PostMapping;
23 29 import org.springframework.web.bind.annotation.RequestBody;
24 30 import org.springframework.web.bind.annotation.RestController;
25 31  
  32 +import java.util.ArrayList;
26 33 import java.util.List;
  34 +import java.util.Map;
  35 +import java.util.Set;
27 36  
28 37 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  38 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
29 39  
30 40 /**
31 41 * IoT 设备 API 实现类
... ... @@ -41,6 +51,12 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi {
41 51 private IotDeviceService deviceService;
42 52 @Resource
43 53 private IotProductService productService;
  54 + @Resource
  55 + @Lazy // 延迟加载,解决循环依赖
  56 + private IotDeviceModbusConfigService modbusConfigService;
  57 + @Resource
  58 + @Lazy // 延迟加载,解决循环依赖
  59 + private IotDeviceModbusPointService modbusPointService;
44 60  
45 61 @Override
46 62 @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/auth")
... ... @@ -64,6 +80,52 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi {
64 80 }
65 81  
66 82 @Override
  83 + @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/modbus/config-list")
  84 + @PermitAll
  85 + @TenantIgnore
  86 + public CommonResult<List<IotModbusDeviceConfigRespDTO>> getModbusDeviceConfigList(
  87 + @RequestBody IotModbusDeviceConfigListReqDTO listReqDTO) {
  88 + // 1. 获取 Modbus 连接配置
  89 + List<IotDeviceModbusConfigDO> configList = modbusConfigService.getDeviceModbusConfigList(listReqDTO);
  90 + if (CollUtil.isEmpty(configList)) {
  91 + return success(new ArrayList<>());
  92 + }
  93 +
  94 + // 2. 组装返回结果
  95 + Set<Long> deviceIds = convertSet(configList, IotDeviceModbusConfigDO::getDeviceId);
  96 + Map<Long, IotDeviceDO> deviceMap = deviceService.getDeviceMap(deviceIds);
  97 + Map<Long, List<IotDeviceModbusPointDO>> pointMap = modbusPointService.getEnabledDeviceModbusPointMapByDeviceIds(deviceIds);
  98 + Map<Long, IotProductDO> productMap = productService.getProductMap(convertSet(deviceMap.values(), IotDeviceDO::getProductId));
  99 + List<IotModbusDeviceConfigRespDTO> result = new ArrayList<>(configList.size());
  100 + for (IotDeviceModbusConfigDO config : configList) {
  101 + // 3.1 获取设备信息
  102 + IotDeviceDO device = deviceMap.get(config.getDeviceId());
  103 + if (device == null) {
  104 + continue;
  105 + }
  106 + // 3.2 按 protocolType 筛选(如果非空)
  107 + if (StrUtil.isNotEmpty(listReqDTO.getProtocolType())) {
  108 + IotProductDO product = productMap.get(device.getProductId());
  109 + if (product == null || ObjUtil.notEqual(listReqDTO.getProtocolType(), product.getProtocolType())) {
  110 + continue;
  111 + }
  112 + }
  113 + // 3.3 获取启用的点位列表
  114 + List<IotDeviceModbusPointDO> pointList = pointMap.get(config.getDeviceId());
  115 + if (CollUtil.isEmpty(pointList)) {
  116 + continue;
  117 + }
  118 +
  119 + // 3.4 构建 IotModbusDeviceConfigRespDTO 对象
  120 + IotModbusDeviceConfigRespDTO configDTO = BeanUtils.toBean(config, IotModbusDeviceConfigRespDTO.class, o ->
  121 + o.setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName())
  122 + .setPoints(BeanUtils.toBean(pointList, IotModbusPointRespDTO.class)));
  123 + result.add(configDTO);
  124 + }
  125 + return success(result);
  126 + }
  127 +
  128 + @Override
67 129 @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/register")
68 130 @PermitAll
69 131 public CommonResult<IotDeviceRegisterRespDTO> registerDevice(@RequestBody IotDeviceRegisterReqDTO reqDTO) {
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusConfigController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.controller.admin.device;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  5 +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigRespVO;
  6 +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigSaveReqVO;
  7 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO;
  8 +import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusConfigService;
  9 +import io.swagger.v3.oas.annotations.Operation;
  10 +import io.swagger.v3.oas.annotations.Parameter;
  11 +import io.swagger.v3.oas.annotations.tags.Tag;
  12 +import jakarta.annotation.Resource;
  13 +import jakarta.validation.Valid;
  14 +import org.springframework.security.access.prepost.PreAuthorize;
  15 +import org.springframework.validation.annotation.Validated;
  16 +import org.springframework.web.bind.annotation.*;
  17 +
  18 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  19 +
  20 +@Tag(name = "管理后台 - IoT 设备 Modbus 连接配置")
  21 +@RestController
  22 +@RequestMapping("/iot/device-modbus-config")
  23 +@Validated
  24 +public class IotDeviceModbusConfigController {
  25 +
  26 + @Resource
  27 + private IotDeviceModbusConfigService modbusConfigService;
  28 +
  29 + @PostMapping("/save")
  30 + @Operation(summary = "保存设备 Modbus 连接配置")
  31 + @PreAuthorize("@ss.hasPermission('iot:device:update')")
  32 + public CommonResult<Boolean> saveDeviceModbusConfig(@Valid @RequestBody IotDeviceModbusConfigSaveReqVO saveReqVO) {
  33 + modbusConfigService.saveDeviceModbusConfig(saveReqVO);
  34 + return success(true);
  35 + }
  36 +
  37 + @GetMapping("/get")
  38 + @Operation(summary = "获得设备 Modbus 连接配置")
  39 + @Parameter(name = "id", description = "编号", example = "1024")
  40 + @Parameter(name = "deviceId", description = "设备编号", example = "2048")
  41 + @PreAuthorize("@ss.hasPermission('iot:device:query')")
  42 + public CommonResult<IotDeviceModbusConfigRespVO> getDeviceModbusConfig(
  43 + @RequestParam(value = "id", required = false) Long id,
  44 + @RequestParam(value = "deviceId", required = false) Long deviceId) {
  45 + IotDeviceModbusConfigDO modbusConfig = null;
  46 + if (id != null) {
  47 + modbusConfig = modbusConfigService.getDeviceModbusConfig(id);
  48 + } else if (deviceId != null) {
  49 + modbusConfig = modbusConfigService.getDeviceModbusConfigByDeviceId(deviceId);
  50 + }
  51 + return success(BeanUtils.toBean(modbusConfig, IotDeviceModbusConfigRespVO.class));
  52 + }
  53 +
  54 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceModbusPointController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.controller.admin.device;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  5 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  6 +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointPageReqVO;
  7 +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointRespVO;
  8 +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointSaveReqVO;
  9 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusPointDO;
  10 +import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusPointService;
  11 +import io.swagger.v3.oas.annotations.Operation;
  12 +import io.swagger.v3.oas.annotations.Parameter;
  13 +import io.swagger.v3.oas.annotations.tags.Tag;
  14 +import jakarta.annotation.Resource;
  15 +import jakarta.validation.Valid;
  16 +import org.springframework.security.access.prepost.PreAuthorize;
  17 +import org.springframework.validation.annotation.Validated;
  18 +import org.springframework.web.bind.annotation.*;
  19 +
  20 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  21 +
  22 +@Tag(name = "管理后台 - IoT 设备 Modbus 点位配置")
  23 +@RestController
  24 +@RequestMapping("/iot/device-modbus-point")
  25 +@Validated
  26 +public class IotDeviceModbusPointController {
  27 +
  28 + @Resource
  29 + private IotDeviceModbusPointService modbusPointService;
  30 +
  31 + @PostMapping("/create")
  32 + @Operation(summary = "创建设备 Modbus 点位配置")
  33 + @PreAuthorize("@ss.hasPermission('iot:device:update')")
  34 + public CommonResult<Long> createDeviceModbusPoint(@Valid @RequestBody IotDeviceModbusPointSaveReqVO createReqVO) {
  35 + return success(modbusPointService.createDeviceModbusPoint(createReqVO));
  36 + }
  37 +
  38 + @PutMapping("/update")
  39 + @Operation(summary = "更新设备 Modbus 点位配置")
  40 + @PreAuthorize("@ss.hasPermission('iot:device:update')")
  41 + public CommonResult<Boolean> updateDeviceModbusPoint(@Valid @RequestBody IotDeviceModbusPointSaveReqVO updateReqVO) {
  42 + modbusPointService.updateDeviceModbusPoint(updateReqVO);
  43 + return success(true);
  44 + }
  45 +
  46 + @DeleteMapping("/delete")
  47 + @Operation(summary = "删除设备 Modbus 点位配置")
  48 + @Parameter(name = "id", description = "编号", required = true)
  49 + @PreAuthorize("@ss.hasPermission('iot:device:update')")
  50 + public CommonResult<Boolean> deleteDeviceModbusPoint(@RequestParam("id") Long id) {
  51 + modbusPointService.deleteDeviceModbusPoint(id);
  52 + return success(true);
  53 + }
  54 +
  55 + @GetMapping("/get")
  56 + @Operation(summary = "获得设备 Modbus 点位配置")
  57 + @Parameter(name = "id", description = "编号", required = true, example = "1024")
  58 + @PreAuthorize("@ss.hasPermission('iot:device:query')")
  59 + public CommonResult<IotDeviceModbusPointRespVO> getDeviceModbusPoint(@RequestParam("id") Long id) {
  60 + IotDeviceModbusPointDO modbusPoint = modbusPointService.getDeviceModbusPoint(id);
  61 + return success(BeanUtils.toBean(modbusPoint, IotDeviceModbusPointRespVO.class));
  62 + }
  63 +
  64 + @GetMapping("/page")
  65 + @Operation(summary = "获得设备 Modbus 点位配置分页")
  66 + @PreAuthorize("@ss.hasPermission('iot:device:query')")
  67 + public CommonResult<PageResult<IotDeviceModbusPointRespVO>> getDeviceModbusPointPage(@Valid IotDeviceModbusPointPageReqVO pageReqVO) {
  68 + PageResult<IotDeviceModbusPointDO> pageResult = modbusPointService.getDeviceModbusPointPage(pageReqVO);
  69 + return success(BeanUtils.toBean(pageResult, IotDeviceModbusPointRespVO.class));
  70 + }
  71 +
  72 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDevicePageReqVO.java
... ... @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.PageParam;
4 4 import cn.iocoder.yudao.framework.common.validation.InEnum;
5   -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
  5 +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum;
6 6 import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum;
7 7 import io.swagger.v3.oas.annotations.media.Schema;
8 8 import lombok.Data;
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/message/IotDeviceMessageRespVO.java
... ... @@ -38,7 +38,7 @@ public class IotDeviceMessageRespVO {
38 38 @Schema(description = "请求编号", example = "req_123")
39 39 private String requestId;
40 40  
41   - @Schema(description = "请求方法", requiredMode = Schema.RequiredMode.REQUIRED, example = "thing.property.report")
  41 + @Schema(description = "请求方法", requiredMode = Schema.RequiredMode.REQUIRED, example = "thing.property.post")
42 42 private String method;
43 43  
44 44 @Schema(description = "请求参数")
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.time.LocalDateTime;
  7 +
  8 +@Schema(description = "管理后台 - IoT 设备 Modbus 连接配置 Response VO")
  9 +@Data
  10 +public class IotDeviceModbusConfigRespVO {
  11 +
  12 + @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  13 + private Long id;
  14 +
  15 + @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  16 + private Long deviceId;
  17 +
  18 + @Schema(description = "设备名称", example = "温湿度传感器")
  19 + private String deviceName;
  20 +
  21 + @Schema(description = "Modbus 服务器 IP 地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "192.168.1.100")
  22 + private String ip;
  23 +
  24 + @Schema(description = "Modbus 端口", requiredMode = Schema.RequiredMode.REQUIRED, example = "502")
  25 + private Integer port;
  26 +
  27 + @Schema(description = "从站地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  28 + private Integer slaveId;
  29 +
  30 + @Schema(description = "连接超时时间(毫秒)", example = "3000")
  31 + private Integer timeout;
  32 +
  33 + @Schema(description = "重试间隔(毫秒)", example = "1000")
  34 + private Integer retryInterval;
  35 +
  36 + @Schema(description = "工作模式", example = "1")
  37 + private Integer mode;
  38 +
  39 + @Schema(description = "数据帧格式", example = "1")
  40 + private Integer frameFormat;
  41 +
  42 + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  43 + private Integer status;
  44 +
  45 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
  46 + private LocalDateTime createTime;
  47 +
  48 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusConfigSaveReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus;
  2 +
  3 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  4 +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum;
  5 +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusModeEnum;
  6 +import io.swagger.v3.oas.annotations.media.Schema;
  7 +import jakarta.validation.constraints.NotNull;
  8 +import lombok.Data;
  9 +
  10 +@Schema(description = "管理后台 - IoT 设备 Modbus 连接配置新增/修改 Request VO")
  11 +@Data
  12 +public class IotDeviceModbusConfigSaveReqVO {
  13 +
  14 + @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  15 + @NotNull(message = "设备编号不能为空")
  16 + private Long deviceId;
  17 +
  18 + @Schema(description = "Modbus 服务器 IP 地址", example = "192.168.1.100")
  19 + private String ip;
  20 +
  21 + @Schema(description = "Modbus 端口", example = "502")
  22 + private Integer port;
  23 +
  24 + @Schema(description = "从站地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  25 + @NotNull(message = "从站地址不能为空")
  26 + private Integer slaveId;
  27 +
  28 + @Schema(description = "连接超时时间(毫秒)", example = "3000")
  29 + private Integer timeout;
  30 +
  31 + @Schema(description = "重试间隔(毫秒)", example = "1000")
  32 + private Integer retryInterval;
  33 +
  34 + @Schema(description = "工作模式", example = "1")
  35 + @InEnum(IotModbusModeEnum.class)
  36 + private Integer mode;
  37 +
  38 + @Schema(description = "数据帧格式", example = "1")
  39 + @InEnum(IotModbusFrameFormatEnum.class)
  40 + private Integer frameFormat;
  41 +
  42 + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  43 + @NotNull(message = "状态不能为空")
  44 + private Integer status;
  45 +
  46 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointPageReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import lombok.Data;
  6 +import lombok.EqualsAndHashCode;
  7 +import lombok.ToString;
  8 +
  9 +@Schema(description = "管理后台 - IoT 设备 Modbus 点位配置分页 Request VO")
  10 +@Data
  11 +@EqualsAndHashCode(callSuper = true)
  12 +@ToString(callSuper = true)
  13 +public class IotDeviceModbusPointPageReqVO extends PageParam {
  14 +
  15 + @Schema(description = "设备编号", example = "1024")
  16 + private Long deviceId;
  17 +
  18 + @Schema(description = "属性标识符", example = "temperature")
  19 + private String identifier;
  20 +
  21 + @Schema(description = "属性名称", example = "温度")
  22 + private String name;
  23 +
  24 + @Schema(description = "Modbus 功能码", example = "3")
  25 + private Integer functionCode;
  26 +
  27 + @Schema(description = "状态", example = "0")
  28 + private Integer status;
  29 +
  30 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.math.BigDecimal;
  7 +import java.time.LocalDateTime;
  8 +
  9 +@Schema(description = "管理后台 - IoT 设备 Modbus 点位配置 Response VO")
  10 +@Data
  11 +public class IotDeviceModbusPointRespVO {
  12 +
  13 + @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  14 + private Long id;
  15 +
  16 + @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  17 + private Long deviceId;
  18 +
  19 + @Schema(description = "物模型属性编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
  20 + private Long thingModelId;
  21 +
  22 + @Schema(description = "属性标识符", requiredMode = Schema.RequiredMode.REQUIRED, example = "temperature")
  23 + private String identifier;
  24 +
  25 + @Schema(description = "属性名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "温度")
  26 + private String name;
  27 +
  28 + @Schema(description = "Modbus 功能码", requiredMode = Schema.RequiredMode.REQUIRED, example = "3")
  29 + private Integer functionCode;
  30 +
  31 + @Schema(description = "寄存器起始地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  32 + private Integer registerAddress;
  33 +
  34 + @Schema(description = "寄存器数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  35 + private Integer registerCount;
  36 +
  37 + @Schema(description = "字节序", requiredMode = Schema.RequiredMode.REQUIRED, example = "AB")
  38 + private String byteOrder;
  39 +
  40 + @Schema(description = "原始数据类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "INT16")
  41 + private String rawDataType;
  42 +
  43 + @Schema(description = "缩放因子", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.0")
  44 + private BigDecimal scale;
  45 +
  46 + @Schema(description = "轮询间隔(毫秒)", requiredMode = Schema.RequiredMode.REQUIRED, example = "5000")
  47 + private Integer pollInterval;
  48 +
  49 + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  50 + private Integer status;
  51 +
  52 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
  53 + private LocalDateTime createTime;
  54 +
  55 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/modbus/IotDeviceModbusPointSaveReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.NotEmpty;
  5 +import jakarta.validation.constraints.NotNull;
  6 +import lombok.Data;
  7 +
  8 +import java.math.BigDecimal;
  9 +
  10 +@Schema(description = "管理后台 - IoT 设备 Modbus 点位配置新增/修改 Request VO")
  11 +@Data
  12 +public class IotDeviceModbusPointSaveReqVO {
  13 +
  14 + @Schema(description = "主键", example = "1")
  15 + private Long id;
  16 +
  17 + @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  18 + @NotNull(message = "设备编号不能为空")
  19 + private Long deviceId;
  20 +
  21 + @Schema(description = "物模型属性编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
  22 + @NotNull(message = "物模型属性编号不能为空")
  23 + private Long thingModelId;
  24 +
  25 + @Schema(description = "Modbus 功能码", requiredMode = Schema.RequiredMode.REQUIRED, example = "3")
  26 + @NotNull(message = "Modbus 功能码不能为空")
  27 + private Integer functionCode;
  28 +
  29 + @Schema(description = "寄存器起始地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  30 + @NotNull(message = "寄存器起始地址不能为空")
  31 + private Integer registerAddress;
  32 +
  33 + @Schema(description = "寄存器数量", example = "1")
  34 + private Integer registerCount;
  35 +
  36 + @Schema(description = "字节序", requiredMode = Schema.RequiredMode.REQUIRED, example = "AB")
  37 + @NotEmpty(message = "字节序不能为空")
  38 + private String byteOrder;
  39 +
  40 + @Schema(description = "原始数据类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "INT16")
  41 + @NotEmpty(message = "原始数据类型不能为空")
  42 + private String rawDataType;
  43 +
  44 + @Schema(description = "缩放因子", example = "1.0")
  45 + private BigDecimal scale;
  46 +
  47 + @Schema(description = "轮询间隔(毫秒)", example = "5000")
  48 + private Integer pollInterval;
  49 +
  50 + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  51 + @NotNull(message = "状态不能为空")
  52 + private Integer status;
  53 +
  54 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.http 0 → 100644
  1 +### 请求 /iot/product/sync-property-table 接口 => 成功
  2 +POST {{baseUrl}}/iot/product/sync-property-table
  3 +Content-Type: application/json
  4 +tenant-id: {{adminTenantId}}
  5 +Authorization: Bearer {{token}}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.java
... ... @@ -141,6 +141,14 @@ public class IotProductController {
141 141 result.getData().getList());
142 142 }
143 143  
  144 + @PostMapping("/sync-property-table")
  145 + @Operation(summary = "同步产品属性表结构到 TDengine")
  146 + @PreAuthorize("@ss.hasPermission('iot:product:update')")
  147 + public CommonResult<Boolean> syncProductPropertyTable() {
  148 + productService.syncProductPropertyTable();
  149 + return success(true);
  150 + }
  151 +
144 152 @GetMapping("/simple-list")
145 153 @Operation(summary = "获取产品的精简信息列表", description = "主要用于前端的下拉选项")
146 154 @Parameter(name = "deviceType", description = "设备类型", example = "1")
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/IotStatisticsController.java
... ... @@ -5,7 +5,7 @@ import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
5 5 import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsDeviceMessageReqVO;
6 6 import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsDeviceMessageSummaryByDateRespVO;
7 7 import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsSummaryRespVO;
8   -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
  8 +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum;
9 9 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
10 10 import cn.iocoder.yudao.module.iot.service.device.message.IotDeviceMessageService;
11 11 import cn.iocoder.yudao.module.iot.service.product.IotProductCategoryService;
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java
... ... @@ -4,7 +4,7 @@ import cn.iocoder.yudao.framework.mybatis.core.type.LongSetTypeHandler;
4 4 import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
5 5 import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO;
6 6 import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
7   -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
  7 +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum;
8 8 import com.baomidou.mybatisplus.annotation.KeySequence;
9 9 import com.baomidou.mybatisplus.annotation.TableField;
10 10 import com.baomidou.mybatisplus.annotation.TableId;
... ... @@ -109,10 +109,6 @@ public class IotDeviceDO extends TenantBaseDO {
109 109 private LocalDateTime activeTime;
110 110  
111 111 /**
112   - * 设备的 IP 地址
113   - */
114   - private String ip;
115   - /**
116 112 * 固件编号
117 113 *
118 114 * 关联 {@link IotOtaFirmwareDO#getId()}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceMessageDO.java
... ... @@ -84,7 +84,7 @@ public class IotDeviceMessageDO {
84 84 * 请求方法
85 85 *
86 86 * 枚举 {@link IotDeviceMessageMethodEnum}
87   - * 例如说:thing.property.report 属性上报
  87 + * 例如说:thing.property.post 属性上报
88 88 */
89 89 private String method;
90 90 /**
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusConfigDO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.dal.dataobject.device;
  2 +
  3 +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
  4 +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum;
  5 +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusModeEnum;
  6 +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
  7 +import com.baomidou.mybatisplus.annotation.KeySequence;
  8 +import com.baomidou.mybatisplus.annotation.TableId;
  9 +import com.baomidou.mybatisplus.annotation.TableName;
  10 +import lombok.*;
  11 +
  12 +/**
  13 + * IoT 设备 Modbus 连接配置 DO
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@TableName("iot_device_modbus_config")
  18 +@KeySequence("iot_device_modbus_config_seq")
  19 +@Data
  20 +@Builder
  21 +@NoArgsConstructor
  22 +@AllArgsConstructor
  23 +public class IotDeviceModbusConfigDO extends TenantBaseDO {
  24 +
  25 + /**
  26 + * 主键
  27 + */
  28 + @TableId
  29 + private Long id;
  30 + /**
  31 + * 产品编号
  32 + *
  33 + * 关联 {@link IotProductDO#getId()}
  34 + */
  35 + private Long productId;
  36 + /**
  37 + * 设备编号
  38 + *
  39 + * 关联 {@link IotDeviceDO#getId()}
  40 + */
  41 + private Long deviceId;
  42 +
  43 + /**
  44 + * Modbus 服务器 IP 地址
  45 + */
  46 + private String ip;
  47 + /**
  48 + * Modbus 服务器端口
  49 + */
  50 + private Integer port;
  51 + /**
  52 + * 从站地址
  53 + */
  54 + private Integer slaveId;
  55 + /**
  56 + * 连接超时时间,单位:毫秒
  57 + */
  58 + private Integer timeout;
  59 + /**
  60 + * 重试间隔,单位:毫秒
  61 + */
  62 + private Integer retryInterval;
  63 + /**
  64 + * 模式
  65 + *
  66 + * @see IotModbusModeEnum
  67 + */
  68 + private Integer mode;
  69 + /**
  70 + * 数据帧格式
  71 + *
  72 + * @see IotModbusFrameFormatEnum
  73 + */
  74 + private Integer frameFormat;
  75 + /**
  76 + * 状态
  77 + *
  78 + * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum}
  79 + */
  80 + private Integer status;
  81 +
  82 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceModbusPointDO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.dal.dataobject.device;
  2 +
  3 +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
  4 +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO;
  5 +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusByteOrderEnum;
  6 +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusRawDataTypeEnum;
  7 +import com.baomidou.mybatisplus.annotation.KeySequence;
  8 +import com.baomidou.mybatisplus.annotation.TableId;
  9 +import com.baomidou.mybatisplus.annotation.TableName;
  10 +import lombok.*;
  11 +
  12 +import java.math.BigDecimal;
  13 +
  14 +/**
  15 + * IoT 设备 Modbus 点位配置 DO
  16 + *
  17 + * @author 芋道源码
  18 + */
  19 +@TableName("iot_device_modbus_point")
  20 +@KeySequence("iot_device_modbus_point_seq")
  21 +@Data
  22 +@Builder
  23 +@NoArgsConstructor
  24 +@AllArgsConstructor
  25 +public class IotDeviceModbusPointDO extends TenantBaseDO {
  26 +
  27 + /**
  28 + * 主键
  29 + */
  30 + @TableId
  31 + private Long id;
  32 + /**
  33 + * 设备编号
  34 + *
  35 + * 关联 {@link IotDeviceDO#getId()}
  36 + */
  37 + private Long deviceId;
  38 + /**
  39 + * 物模型属性编号
  40 + *
  41 + * 关联 {@link IotThingModelDO#getId()}
  42 + */
  43 + private Long thingModelId;
  44 + /**
  45 + * 属性标识符
  46 + *
  47 + * 冗余 {@link IotThingModelDO#getIdentifier()}
  48 + */
  49 + private String identifier;
  50 + /**
  51 + * 属性名称
  52 + *
  53 + * 冗余 {@link IotThingModelDO#getName()}
  54 + */
  55 + private String name;
  56 +
  57 + // ========== Modbus 协议配置 ==========
  58 +
  59 + /**
  60 + * Modbus 功能码
  61 + *
  62 + * 取值范围:FC01-04(读线圈、读离散输入、读保持寄存器、读输入寄存器)
  63 + */
  64 + private Integer functionCode;
  65 + /**
  66 + * 寄存器起始地址
  67 + */
  68 + private Integer registerAddress;
  69 + /**
  70 + * 寄存器数量
  71 + */
  72 + private Integer registerCount;
  73 + /**
  74 + * 字节序
  75 + *
  76 + * 枚举 {@link IotModbusByteOrderEnum}
  77 + */
  78 + private String byteOrder;
  79 + /**
  80 + * 原始数据类型
  81 + *
  82 + * 枚举 {@link IotModbusRawDataTypeEnum}
  83 + */
  84 + private String rawDataType;
  85 + /**
  86 + * 缩放因子
  87 + */
  88 + private BigDecimal scale;
  89 + /**
  90 + * 轮询间隔(毫秒)
  91 + */
  92 + private Integer pollInterval;
  93 + /**
  94 + * 状态
  95 + *
  96 + * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum}
  97 + */
  98 + private Integer status;
  99 +
  100 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusConfigMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.dal.mysql.device;
  2 +
  3 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  4 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  5 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO;
  6 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO;
  7 +import org.apache.ibatis.annotations.Mapper;
  8 +
  9 +import java.util.List;
  10 +
  11 +/**
  12 + * IoT 设备 Modbus 连接配置 Mapper
  13 + *
  14 + * @author 芋道源码
  15 + */
  16 +@Mapper
  17 +public interface IotDeviceModbusConfigMapper extends BaseMapperX<IotDeviceModbusConfigDO> {
  18 +
  19 + default IotDeviceModbusConfigDO selectByDeviceId(Long deviceId) {
  20 + return selectOne(IotDeviceModbusConfigDO::getDeviceId, deviceId);
  21 + }
  22 +
  23 + default List<IotDeviceModbusConfigDO> selectList(IotModbusDeviceConfigListReqDTO reqDTO) {
  24 + return selectList(new LambdaQueryWrapperX<IotDeviceModbusConfigDO>()
  25 + .eqIfPresent(IotDeviceModbusConfigDO::getStatus, reqDTO.getStatus())
  26 + .eqIfPresent(IotDeviceModbusConfigDO::getMode, reqDTO.getMode())
  27 + .inIfPresent(IotDeviceModbusConfigDO::getDeviceId, reqDTO.getDeviceIds()));
  28 + }
  29 +
  30 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceModbusPointMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.dal.mysql.device;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  5 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  6 +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointPageReqVO;
  7 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusPointDO;
  8 +import org.apache.ibatis.annotations.Mapper;
  9 +
  10 +import java.util.Collection;
  11 +import java.util.List;
  12 +
  13 +/**
  14 + * IoT 设备 Modbus 点位配置 Mapper
  15 + *
  16 + * @author 芋道源码
  17 + */
  18 +@Mapper
  19 +public interface IotDeviceModbusPointMapper extends BaseMapperX<IotDeviceModbusPointDO> {
  20 +
  21 + default PageResult<IotDeviceModbusPointDO> selectPage(IotDeviceModbusPointPageReqVO reqVO) {
  22 + return selectPage(reqVO, new LambdaQueryWrapperX<IotDeviceModbusPointDO>()
  23 + .eqIfPresent(IotDeviceModbusPointDO::getDeviceId, reqVO.getDeviceId())
  24 + .likeIfPresent(IotDeviceModbusPointDO::getIdentifier, reqVO.getIdentifier())
  25 + .likeIfPresent(IotDeviceModbusPointDO::getName, reqVO.getName())
  26 + .eqIfPresent(IotDeviceModbusPointDO::getFunctionCode, reqVO.getFunctionCode())
  27 + .eqIfPresent(IotDeviceModbusPointDO::getStatus, reqVO.getStatus())
  28 + .orderByDesc(IotDeviceModbusPointDO::getId));
  29 + }
  30 +
  31 + default List<IotDeviceModbusPointDO> selectListByDeviceIdsAndStatus(Collection<Long> deviceIds, Integer status) {
  32 + return selectList(new LambdaQueryWrapperX<IotDeviceModbusPointDO>()
  33 + .in(IotDeviceModbusPointDO::getDeviceId, deviceIds)
  34 + .eq(IotDeviceModbusPointDO::getStatus, status));
  35 + }
  36 +
  37 + default IotDeviceModbusPointDO selectByDeviceIdAndIdentifier(Long deviceId, String identifier) {
  38 + return selectOne(IotDeviceModbusPointDO::getDeviceId, deviceId,
  39 + IotDeviceModbusPointDO::getIdentifier, identifier);
  40 + }
  41 +
  42 + default void updateByThingModelId(Long thingModelId, IotDeviceModbusPointDO updateObj) {
  43 + update(updateObj, new LambdaQueryWrapperX<IotDeviceModbusPointDO>()
  44 + .eq(IotDeviceModbusPointDO::getThingModelId, thingModelId));
  45 + }
  46 +
  47 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductMapper.java
... ... @@ -38,6 +38,10 @@ public interface IotProductMapper extends BaseMapperX&lt;IotProductDO&gt; {
38 38 .apply("LOWER(product_key) = {0}", productKey.toLowerCase()));
39 39 }
40 40  
  41 + default List<IotProductDO> selectListByStatus(Integer status) {
  42 + return selectList(IotProductDO::getStatus, status);
  43 + }
  44 +
41 45 default Long selectCountByCreateTime(@Nullable LocalDateTime createTime) {
42 46 return selectCount(new LambdaQueryWrapperX<IotProductDO>()
43 47 .geIfPresent(IotProductDO::getCreateTime, createTime));
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java
... ... @@ -54,6 +54,14 @@ public interface ErrorCodeConstants {
54 54 ErrorCode DEVICE_GROUP_NOT_EXISTS = new ErrorCode(1_050_005_000, "设备分组不存在");
55 55 ErrorCode DEVICE_GROUP_DELETE_FAIL_DEVICE_EXISTS = new ErrorCode(1_050_005_001, "设备分组下存在设备,不允许删除");
56 56  
  57 + // ========== 设备 Modbus 配置 1-050-006-000 ==========
  58 + ErrorCode DEVICE_MODBUS_CONFIG_NOT_EXISTS = new ErrorCode(1_050_006_000, "设备 Modbus 连接配置不存在");
  59 + ErrorCode DEVICE_MODBUS_CONFIG_EXISTS = new ErrorCode(1_050_006_001, "设备 Modbus 连接配置已存在");
  60 +
  61 + // ========== 设备 Modbus 点位 1-050-007-000 ==========
  62 + ErrorCode DEVICE_MODBUS_POINT_NOT_EXISTS = new ErrorCode(1_050_007_000, "设备 Modbus 点位配置不存在");
  63 + ErrorCode DEVICE_MODBUS_POINT_EXISTS = new ErrorCode(1_050_007_001, "设备 Modbus 点位配置已存在");
  64 +
57 65 // ========== OTA 固件相关 1-050-008-000 ==========
58 66  
59 67 ErrorCode OTA_FIRMWARE_NOT_EXISTS = new ErrorCode(1_050_008_000, "固件信息不存在");
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotDataSinkTypeEnum.java
... ... @@ -19,9 +19,9 @@ public enum IotDataSinkTypeEnum implements ArrayValuable&lt;Integer&gt; {
19 19 TCP(2, "TCP"),
20 20 WEBSOCKET(3, "WebSocket"),
21 21  
22   - MQTT(10, "MQTT"), // TODO 待实现;
  22 + MQTT(10, "MQTT"), // TODO @puhui999:待实现;
23 23  
24   - DATABASE(20, "Database"), // TODO @puhui999:待实现;可以简单点,对应的表名是什么,字段先固定了。
  24 + DATABASE(20, "Database"), // TODO @puhui999:待实现;
25 25 REDIS(21, "Redis"),
26 26  
27 27 ROCKETMQ(30, "RocketMQ"),
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/device/IotDeviceOfflineCheckJob.java
... ... @@ -4,7 +4,7 @@ import cn.hutool.core.collection.CollUtil;
4 4 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
5 5 import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
6 6 import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
7   -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
  7 +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum;
8 8 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
9 9 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
10 10 import cn.iocoder.yudao.module.iot.framework.iot.config.YudaoIotProperties;
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/ota/IotOtaUpgradeJob.java
... ... @@ -4,7 +4,7 @@ import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.util.StrUtil;
5 5 import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
6 6 import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
7   -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
  7 +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum;
8 8 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
9 9 import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO;
10 10 import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaTaskRecordDO;
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDeviceMessageSubscriber.java
... ... @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.iot.mq.consumer.device;
3 3 import cn.hutool.core.util.ObjectUtil;
4 4 import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
5 5 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
6   -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
  6 +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum;
7 7 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
8 8 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
9 9 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
... ... @@ -67,7 +67,6 @@ public class IotDeviceMessageSubscriber implements IotMessageSubscriber&lt;IotDevic
67 67 IotDeviceDO device = deviceService.validateDeviceExistsFromCache(message.getDeviceId());
68 68 devicePropertyService.updateDeviceReportTimeAsync(device.getId(), LocalDateTime.now());
69 69 // 1.2 更新设备的连接 server
70   - // TODO 芋艿:HTTP 网关的上行消息,不应该更新 serverId,会覆盖掉 MQTT 等长连接的 serverId,导致下行消息无法发送。
71 70 devicePropertyService.updateDeviceServerIdAsync(device.getId(), message.getServerId());
72 71  
73 72 // 2. 未上线的设备,强制上线
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotDataRuleMessageHandler.java renamed to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotDataRuleMessageSubscriber.java
... ... @@ -17,7 +17,7 @@ import org.springframework.stereotype.Component;
17 17 */
18 18 @Component
19 19 @Slf4j
20   -public class IotDataRuleMessageHandler implements IotMessageSubscriber<IotDeviceMessage> {
  20 +public class IotDataRuleMessageSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
21 21  
22 22 @Resource
23 23 private IotDataRuleService dataRuleService;
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotSceneRuleMessageHandler.java renamed to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotSceneRuleMessageSubscriber.java
... ... @@ -9,7 +9,6 @@ import jakarta.annotation.Resource;
9 9 import lombok.extern.slf4j.Slf4j;
10 10 import org.springframework.stereotype.Component;
11 11  
12   -// TODO @puhui999:后面重构哈
13 12 /**
14 13 * 针对 {@link IotDeviceMessage} 的消费者,处理规则场景
15 14 *
... ... @@ -17,7 +16,7 @@ import org.springframework.stereotype.Component;
17 16 */
18 17 @Component
19 18 @Slf4j
20   -public class IotSceneRuleMessageHandler implements IotMessageSubscriber<IotDeviceMessage> {
  19 +public class IotSceneRuleMessageSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
21 20  
22 21 @Resource
23 22 private IotSceneRuleService sceneRuleService;
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigService.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.device;
  2 +
  3 +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigSaveReqVO;
  4 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO;
  5 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO;
  6 +import jakarta.validation.Valid;
  7 +
  8 +import java.util.List;
  9 +
  10 +/**
  11 + * IoT 设备 Modbus 连接配置 Service 接口
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +public interface IotDeviceModbusConfigService {
  16 +
  17 + /**
  18 + * 保存设备 Modbus 连接配置(新增或更新)
  19 + *
  20 + * @param saveReqVO 保存信息
  21 + */
  22 + void saveDeviceModbusConfig(@Valid IotDeviceModbusConfigSaveReqVO saveReqVO);
  23 +
  24 + /**
  25 + * 获得设备 Modbus 连接配置
  26 + *
  27 + * @param id 编号
  28 + * @return 设备 Modbus 连接配置
  29 + */
  30 + IotDeviceModbusConfigDO getDeviceModbusConfig(Long id);
  31 +
  32 + /**
  33 + * 根据设备编号获得 Modbus 连接配置
  34 + *
  35 + * @param deviceId 设备编号
  36 + * @return 设备 Modbus 连接配置
  37 + */
  38 + IotDeviceModbusConfigDO getDeviceModbusConfigByDeviceId(Long deviceId);
  39 +
  40 + /**
  41 + * 获得 Modbus 连接配置列表
  42 + *
  43 + * @param listReqDTO 查询参数
  44 + * @return Modbus 连接配置列表
  45 + */
  46 + List<IotDeviceModbusConfigDO> getDeviceModbusConfigList(IotModbusDeviceConfigListReqDTO listReqDTO);
  47 +
  48 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusConfigServiceImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.device;
  2 +
  3 +import cn.hutool.core.lang.Assert;
  4 +import cn.hutool.core.util.StrUtil;
  5 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  6 +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusConfigSaveReqVO;
  7 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO;
  8 +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
  9 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusConfigDO;
  10 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
  11 +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
  12 +import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceModbusConfigMapper;
  13 +import cn.iocoder.yudao.module.iot.service.product.IotProductService;
  14 +import jakarta.annotation.Resource;
  15 +import org.springframework.stereotype.Service;
  16 +import org.springframework.validation.annotation.Validated;
  17 +
  18 +import java.util.List;
  19 +
  20 +/**
  21 + * IoT 设备 Modbus 连接配置 Service 实现类
  22 + *
  23 + * @author 芋道源码
  24 + */
  25 +@Service
  26 +@Validated
  27 +public class IotDeviceModbusConfigServiceImpl implements IotDeviceModbusConfigService {
  28 +
  29 + @Resource
  30 + private IotDeviceModbusConfigMapper modbusConfigMapper;
  31 +
  32 + @Resource
  33 + private IotDeviceService deviceService;
  34 + @Resource
  35 + private IotProductService productService;
  36 +
  37 + @Override
  38 + public void saveDeviceModbusConfig(IotDeviceModbusConfigSaveReqVO saveReqVO) {
  39 + // 1.1 校验设备存在
  40 + IotDeviceDO device = deviceService.validateDeviceExists(saveReqVO.getDeviceId());
  41 + // 1.2 根据产品 protocolType 条件校验
  42 + IotProductDO product = productService.getProduct(device.getProductId());
  43 + Assert.notNull(product, "产品不存在");
  44 + validateModbusConfigByProtocolType(saveReqVO, product.getProtocolType());
  45 +
  46 + // 2. 根据数据库中是否已有配置,决定是新增还是更新
  47 + IotDeviceModbusConfigDO existConfig = modbusConfigMapper.selectByDeviceId(saveReqVO.getDeviceId());
  48 + if (existConfig == null) {
  49 + IotDeviceModbusConfigDO modbusConfig = BeanUtils.toBean(saveReqVO, IotDeviceModbusConfigDO.class);
  50 + modbusConfigMapper.insert(modbusConfig);
  51 + } else {
  52 + IotDeviceModbusConfigDO updateObj = BeanUtils.toBean(saveReqVO, IotDeviceModbusConfigDO.class,
  53 + o -> o.setId(existConfig.getId()));
  54 + modbusConfigMapper.updateById(updateObj);
  55 + }
  56 + }
  57 +
  58 + @Override
  59 + public IotDeviceModbusConfigDO getDeviceModbusConfig(Long id) {
  60 + return modbusConfigMapper.selectById(id);
  61 + }
  62 +
  63 + @Override
  64 + public IotDeviceModbusConfigDO getDeviceModbusConfigByDeviceId(Long deviceId) {
  65 + return modbusConfigMapper.selectByDeviceId(deviceId);
  66 + }
  67 +
  68 + @Override
  69 + public List<IotDeviceModbusConfigDO> getDeviceModbusConfigList(IotModbusDeviceConfigListReqDTO listReqDTO) {
  70 + return modbusConfigMapper.selectList(listReqDTO);
  71 + }
  72 +
  73 + private void validateModbusConfigByProtocolType(IotDeviceModbusConfigSaveReqVO saveReqVO, String protocolType) {
  74 + IotProtocolTypeEnum protocolTypeEnum = IotProtocolTypeEnum.of(protocolType);
  75 + if (protocolTypeEnum == null) {
  76 + return;
  77 + }
  78 + if (protocolTypeEnum == IotProtocolTypeEnum.MODBUS_TCP_CLIENT) {
  79 + Assert.isTrue(StrUtil.isNotEmpty(saveReqVO.getIp()), "Client 模式下,IP 地址不能为空");
  80 + Assert.notNull(saveReqVO.getPort(), "Client 模式下,端口不能为空");
  81 + Assert.notNull(saveReqVO.getTimeout(), "Client 模式下,连接超时时间不能为空");
  82 + Assert.notNull(saveReqVO.getRetryInterval(), "Client 模式下,重试间隔不能为空");
  83 + } else if (protocolTypeEnum == IotProtocolTypeEnum.MODBUS_TCP_SERVER) {
  84 + Assert.notNull(saveReqVO.getMode(), "Server 模式下,工作模式不能为空");
  85 + Assert.notNull(saveReqVO.getFrameFormat(), "Server 模式下,数据帧格式不能为空");
  86 + }
  87 + }
  88 +
  89 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointService.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.device;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointPageReqVO;
  5 +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointSaveReqVO;
  6 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusPointDO;
  7 +import jakarta.validation.Valid;
  8 +
  9 +import java.util.Collection;
  10 +import java.util.List;
  11 +import java.util.Map;
  12 +
  13 +/**
  14 + * IoT 设备 Modbus 点位配置 Service 接口
  15 + *
  16 + * @author 芋道源码
  17 + */
  18 +public interface IotDeviceModbusPointService {
  19 +
  20 + /**
  21 + * 创建设备 Modbus 点位配置
  22 + *
  23 + * @param createReqVO 创建信息
  24 + * @return 编号
  25 + */
  26 + Long createDeviceModbusPoint(@Valid IotDeviceModbusPointSaveReqVO createReqVO);
  27 +
  28 + /**
  29 + * 更新设备 Modbus 点位配置
  30 + *
  31 + * @param updateReqVO 更新信息
  32 + */
  33 + void updateDeviceModbusPoint(@Valid IotDeviceModbusPointSaveReqVO updateReqVO);
  34 +
  35 + /**
  36 + * 删除设备 Modbus 点位配置
  37 + *
  38 + * @param id 编号
  39 + */
  40 + void deleteDeviceModbusPoint(Long id);
  41 +
  42 + /**
  43 + * 获得设备 Modbus 点位配置
  44 + *
  45 + * @param id 编号
  46 + * @return 设备 Modbus 点位配置
  47 + */
  48 + IotDeviceModbusPointDO getDeviceModbusPoint(Long id);
  49 +
  50 + /**
  51 + * 获得设备 Modbus 点位配置分页
  52 + *
  53 + * @param pageReqVO 分页查询
  54 + * @return 设备 Modbus 点位配置分页
  55 + */
  56 + PageResult<IotDeviceModbusPointDO> getDeviceModbusPointPage(IotDeviceModbusPointPageReqVO pageReqVO);
  57 +
  58 + /**
  59 + * 物模型变更时,更新关联点位的冗余字段(identifier、name)
  60 + *
  61 + * @param thingModelId 物模型编号
  62 + * @param identifier 物模型标识符
  63 + * @param name 物模型名称
  64 + */
  65 + void updateDeviceModbusPointByThingModel(Long thingModelId, String identifier, String name);
  66 +
  67 + /**
  68 + * 根据设备编号批量获得启用的点位配置 Map
  69 + *
  70 + * @param deviceIds 设备编号集合
  71 + * @return 设备点位 Map,key 为设备编号,value 为点位配置列表
  72 + */
  73 + Map<Long, List<IotDeviceModbusPointDO>> getEnabledDeviceModbusPointMapByDeviceIds(Collection<Long> deviceIds);
  74 +
  75 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceModbusPointServiceImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.device;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.ObjUtil;
  5 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  6 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  7 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  8 +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointPageReqVO;
  9 +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.modbus.IotDeviceModbusPointSaveReqVO;
  10 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceModbusPointDO;
  11 +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO;
  12 +import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceModbusPointMapper;
  13 +import cn.iocoder.yudao.module.iot.service.thingmodel.IotThingModelService;
  14 +import jakarta.annotation.Resource;
  15 +import org.springframework.stereotype.Service;
  16 +import org.springframework.validation.annotation.Validated;
  17 +
  18 +import java.util.Collection;
  19 +import java.util.Collections;
  20 +import java.util.List;
  21 +import java.util.Map;
  22 +
  23 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  24 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap;
  25 +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*;
  26 +
  27 +/**
  28 + * IoT 设备 Modbus 点位配置 Service 实现类
  29 + *
  30 + * @author 芋道源码
  31 + */
  32 +@Service
  33 +@Validated
  34 +public class IotDeviceModbusPointServiceImpl implements IotDeviceModbusPointService {
  35 +
  36 + @Resource
  37 + private IotDeviceModbusPointMapper modbusPointMapper;
  38 +
  39 + @Resource
  40 + private IotDeviceService deviceService;
  41 +
  42 + @Resource
  43 + private IotThingModelService thingModelService;
  44 +
  45 + @Override
  46 + public Long createDeviceModbusPoint(IotDeviceModbusPointSaveReqVO createReqVO) {
  47 + // 1.1 校验设备存在
  48 + deviceService.validateDeviceExists(createReqVO.getDeviceId());
  49 + // 1.2 校验物模型属性存在
  50 + IotThingModelDO thingModel = validateThingModelExists(createReqVO.getThingModelId());
  51 + // 1.3 校验同一设备下点位唯一性(基于 identifier)
  52 + validateDeviceModbusPointUnique(createReqVO.getDeviceId(), thingModel.getIdentifier(), null);
  53 +
  54 + // 2. 插入
  55 + IotDeviceModbusPointDO modbusPoint = BeanUtils.toBean(createReqVO, IotDeviceModbusPointDO.class,
  56 + o -> o.setIdentifier(thingModel.getIdentifier()).setName(thingModel.getName()));
  57 + modbusPointMapper.insert(modbusPoint);
  58 + return modbusPoint.getId();
  59 + }
  60 +
  61 + @Override
  62 + public void updateDeviceModbusPoint(IotDeviceModbusPointSaveReqVO updateReqVO) {
  63 + // 1.1 校验存在
  64 + validateDeviceModbusPointExists(updateReqVO.getId());
  65 + // 1.2 校验设备存在
  66 + deviceService.validateDeviceExists(updateReqVO.getDeviceId());
  67 + // 1.3 校验物模型属性存在
  68 + IotThingModelDO thingModel = validateThingModelExists(updateReqVO.getThingModelId());
  69 + // 1.4 校验同一设备下点位唯一性
  70 + validateDeviceModbusPointUnique(updateReqVO.getDeviceId(), thingModel.getIdentifier(), updateReqVO.getId());
  71 +
  72 + // 2. 更新
  73 + IotDeviceModbusPointDO updateObj = BeanUtils.toBean(updateReqVO, IotDeviceModbusPointDO.class,
  74 + o -> o.setIdentifier(thingModel.getIdentifier()).setName(thingModel.getName()));
  75 + modbusPointMapper.updateById(updateObj);
  76 + }
  77 +
  78 + @Override
  79 + public void updateDeviceModbusPointByThingModel(Long thingModelId, String identifier, String name) {
  80 + IotDeviceModbusPointDO updateObj = new IotDeviceModbusPointDO()
  81 + .setIdentifier(identifier).setName(name);
  82 + modbusPointMapper.updateByThingModelId(thingModelId, updateObj);
  83 + }
  84 +
  85 + private IotThingModelDO validateThingModelExists(Long id) {
  86 + IotThingModelDO thingModel = thingModelService.getThingModel(id);
  87 + if (thingModel == null) {
  88 + throw exception(THING_MODEL_NOT_EXISTS);
  89 + }
  90 + return thingModel;
  91 + }
  92 +
  93 + @Override
  94 + public void deleteDeviceModbusPoint(Long id) {
  95 + // 校验存在
  96 + validateDeviceModbusPointExists(id);
  97 + // 删除
  98 + modbusPointMapper.deleteById(id);
  99 + }
  100 +
  101 + private void validateDeviceModbusPointExists(Long id) {
  102 + IotDeviceModbusPointDO point = modbusPointMapper.selectById(id);
  103 + if (point == null) {
  104 + throw exception(DEVICE_MODBUS_POINT_NOT_EXISTS);
  105 + }
  106 + }
  107 +
  108 + private void validateDeviceModbusPointUnique(Long deviceId, String identifier, Long excludeId) {
  109 + IotDeviceModbusPointDO point = modbusPointMapper.selectByDeviceIdAndIdentifier(deviceId, identifier);
  110 + if (point != null && ObjUtil.notEqual(point.getId(), excludeId)) {
  111 + throw exception(DEVICE_MODBUS_POINT_EXISTS);
  112 + }
  113 + }
  114 +
  115 + @Override
  116 + public IotDeviceModbusPointDO getDeviceModbusPoint(Long id) {
  117 + return modbusPointMapper.selectById(id);
  118 + }
  119 +
  120 + @Override
  121 + public PageResult<IotDeviceModbusPointDO> getDeviceModbusPointPage(IotDeviceModbusPointPageReqVO pageReqVO) {
  122 + return modbusPointMapper.selectPage(pageReqVO);
  123 + }
  124 +
  125 + @Override
  126 + public Map<Long, List<IotDeviceModbusPointDO>> getEnabledDeviceModbusPointMapByDeviceIds(Collection<Long> deviceIds) {
  127 + if (CollUtil.isEmpty(deviceIds)) {
  128 + return Collections.emptyMap();
  129 + }
  130 + List<IotDeviceModbusPointDO> pointList = modbusPointMapper.selectListByDeviceIdsAndStatus(
  131 + deviceIds, CommonStatusEnum.ENABLE.getStatus());
  132 + return convertMultiMap(pointList, IotDeviceModbusPointDO::getDeviceId);
  133 + }
  134 +
  135 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java
... ... @@ -4,7 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 4 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*;
5 5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
6 6 import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
7   -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
  7 +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum;
8 8 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
9 9 import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
10 10 import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java
... ... @@ -17,7 +17,7 @@ import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*;
17 17 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
18 18 import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
19 19 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
20   -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
  20 +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum;
21 21 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
22 22 import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
23 23 import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaTaskRecordServiceImpl.java
... ... @@ -3,11 +3,15 @@ package cn.iocoder.yudao.module.iot.service.ota;
3 3 import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.convert.Convert;
5 5 import cn.hutool.core.lang.Assert;
6   -import cn.hutool.core.map.MapUtil;
7 6 import cn.hutool.core.util.StrUtil;
8 7 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  8 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  9 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
9 10 import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.task.record.IotOtaTaskRecordPageReqVO;
  11 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
10 12 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  13 +import cn.iocoder.yudao.module.iot.core.topic.ota.IotDeviceOtaProgressReqDTO;
  14 +import cn.iocoder.yudao.module.iot.core.topic.ota.IotDeviceOtaUpgradeReqDTO;
11 15 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
12 16 import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO;
13 17 import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaTaskRecordDO;
... ... @@ -133,9 +137,9 @@ public class IotOtaTaskRecordServiceImpl implements IotOtaTaskRecordService {
133 137 public boolean pushOtaTaskRecord(IotOtaTaskRecordDO record, IotOtaFirmwareDO fireware, IotDeviceDO device) {
134 138 try {
135 139 // 1. 推送 OTA 任务记录
136   - IotDeviceMessage message = IotDeviceMessage.buildOtaUpgrade(
137   - fireware.getVersion(), fireware.getFileUrl(), fireware.getFileSize(),
138   - fireware.getFileDigestAlgorithm(), fireware.getFileDigestValue());
  140 + IotDeviceOtaUpgradeReqDTO params = BeanUtils.toBean(fireware, IotDeviceOtaUpgradeReqDTO.class);
  141 + IotDeviceMessage message = IotDeviceMessage.requestOf(
  142 + IotDeviceMessageMethodEnum.OTA_UPGRADE.getMethod(), params);
139 143 deviceMessageService.sendDeviceMessage(message, device);
140 144  
141 145 // 2. 更新 OTA 升级记录状态为进行中
... ... @@ -163,17 +167,16 @@ public class IotOtaTaskRecordServiceImpl implements IotOtaTaskRecordService {
163 167  
164 168 @Override
165 169 @Transactional(rollbackFor = Exception.class)
166   - @SuppressWarnings("unchecked")
167 170 public void updateOtaRecordProgress(IotDeviceDO device, IotDeviceMessage message) {
168 171 // 1.1 参数解析
169   - Map<String, Object> params = (Map<String, Object>) message.getParams();
170   - String version = MapUtil.getStr(params, "version");
  172 + IotDeviceOtaProgressReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceOtaProgressReqDTO.class);
  173 + String version = params.getVersion();
171 174 Assert.notBlank(version, "version 不能为空");
172   - Integer status = MapUtil.getInt(params, "status");
  175 + Integer status = params.getStatus();
173 176 Assert.notNull(status, "status 不能为空");
174 177 Assert.notNull(IotOtaTaskRecordStatusEnum.of(status), "status 状态不正确");
175   - String description = MapUtil.getStr(params, "description");
176   - Integer progress = MapUtil.getInt(params, "progress");
  178 + String description = params.getDescription();
  179 + Integer progress = params.getProgress();
177 180 Assert.notNull(progress, "progress 不能为空");
178 181 Assert.isTrue(progress >= 0 && progress <= 100, "progress 必须在 0-100 之间");
179 182 // 1.2 查询 OTA 升级记录
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductService.java
... ... @@ -10,6 +10,9 @@ import javax.annotation.Nullable;
10 10 import java.time.LocalDateTime;
11 11 import java.util.Collection;
12 12 import java.util.List;
  13 +import java.util.Map;
  14 +
  15 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
13 16  
14 17 /**
15 18 * IoT 产品 Service 接口
... ... @@ -122,10 +125,35 @@ public interface IotProductService {
122 125 Long getProductCount(@Nullable LocalDateTime createTime);
123 126  
124 127 /**
  128 + * 批量获得产品列表
  129 + *
  130 + * @param ids 产品编号集合
  131 + * @return 产品列表
  132 + */
  133 + List<IotProductDO> getProductList(Collection<Long> ids);
  134 +
  135 + /**
  136 + * 批量获得产品 Map
  137 + *
  138 + * @param ids 产品编号集合
  139 + * @return 产品 Map(key: 产品编号, value: 产品)
  140 + */
  141 + default Map<Long, IotProductDO> getProductMap(Collection<Long> ids) {
  142 + return convertMap(getProductList(ids), IotProductDO::getId);
  143 + }
  144 +
  145 + /**
125 146 * 批量校验产品存在
126 147 *
127 148 * @param ids 产品编号集合
128 149 */
129 150 void validateProductsExist(Collection<Long> ids);
130 151  
  152 + /**
  153 + * 同步产品的 TDengine 表结构
  154 + *
  155 + * 目的:当 MySQL 和 TDengine 不同步时,强制将已发布产品的表结构同步到 TDengine 中
  156 + */
  157 + void syncProductPropertyTable();
  158 +
131 159 }
132 160 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java
... ... @@ -15,8 +15,10 @@ import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
15 15 import cn.iocoder.yudao.module.iot.service.device.property.IotDevicePropertyService;
16 16 import com.baomidou.dynamic.datasource.annotation.DSTransactional;
17 17 import jakarta.annotation.Resource;
  18 +import lombok.extern.slf4j.Slf4j;
18 19 import org.springframework.cache.annotation.CacheEvict;
19 20 import org.springframework.cache.annotation.Cacheable;
  21 +import org.springframework.context.annotation.Lazy;
20 22 import org.springframework.stereotype.Service;
21 23 import org.springframework.validation.annotation.Validated;
22 24  
... ... @@ -33,6 +35,7 @@ import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*;
33 35 *
34 36 * @author ahh
35 37 */
  38 +@Slf4j
36 39 @Service
37 40 @Validated
38 41 public class IotProductServiceImpl implements IotProductService {
... ... @@ -41,9 +44,10 @@ public class IotProductServiceImpl implements IotProductService {
41 44 private IotProductMapper productMapper;
42 45  
43 46 @Resource
44   - private IotDevicePropertyService devicePropertyDataService;
45   - @Resource
46 47 private IotDeviceService deviceService;
  48 + @Resource
  49 + @Lazy // 延迟加载,避免循环依赖
  50 + private IotDevicePropertyService devicePropertyDataService;
47 51  
48 52 @Override
49 53 public Long createProduct(IotProductSaveReqVO createReqVO) {
... ... @@ -172,6 +176,32 @@ public class IotProductServiceImpl implements IotProductService {
172 176 }
173 177  
174 178 @Override
  179 + public List<IotProductDO> getProductList(Collection<Long> ids) {
  180 + return productMapper.selectByIds(ids);
  181 + }
  182 +
  183 + @Override
  184 + public void syncProductPropertyTable() {
  185 + // 1. 获取所有已发布的产品
  186 + List<IotProductDO> products = productMapper.selectListByStatus(
  187 + IotProductStatusEnum.PUBLISHED.getStatus());
  188 + log.info("[syncProductPropertyTable][开始同步,已发布产品数量({})]", products.size());
  189 +
  190 + // 2. 遍历同步 TDengine 表结构(创建产品超级表数据模型)
  191 + int successCount = 0;
  192 + for (IotProductDO product : products) {
  193 + try {
  194 + devicePropertyDataService.defineDevicePropertyData(product.getId());
  195 + successCount++;
  196 + log.info("[syncProductPropertyTable][产品({}/{}) 同步成功]", product.getId(), product.getName());
  197 + } catch (Exception e) {
  198 + log.error("[syncProductPropertyTable][产品({}/{}) 同步失败]", product.getId(), product.getName(), e);
  199 + }
  200 + }
  201 + log.info("[syncProductPropertyTable][同步完成,成功({}/{})个]", successCount, products.size());
  202 + }
  203 +
  204 + @Override
175 205 public void validateProductsExist(Collection<Long> ids) {
176 206 if (CollUtil.isEmpty(ids)) {
177 207 return;
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotDataRuleCacheableAction.java
... ... @@ -14,8 +14,6 @@ import java.time.Duration;
14 14  
15 15 // TODO @芋艿:数据库
16 16 // TODO @芋艿:mqtt
17   -// TODO @芋艿:tcp
18   -// TODO @芋艿:websocket
19 17  
20 18 /**
21 19 * 可缓存的 {@link IotDataRuleAction} 抽象实现
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDeviceControlSceneRuleAction.java renamed to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDevicePropertySetSceneRuleAction.java
... ... @@ -15,7 +15,6 @@ import lombok.extern.slf4j.Slf4j;
15 15 import org.springframework.stereotype.Component;
16 16  
17 17 import java.util.List;
18   -import java.util.Map;
19 18  
20 19 /**
21 20 * IoT 设备属性设置的 {@link IotSceneRuleAction} 实现类
... ... @@ -24,7 +23,7 @@ import java.util.Map;
24 23 */
25 24 @Component
26 25 @Slf4j
27   -public class IotDeviceControlSceneRuleAction implements IotSceneRuleAction {
  26 +public class IotDevicePropertySetSceneRuleAction implements IotSceneRuleAction {
28 27  
29 28 @Resource
30 29 private IotDeviceService deviceService;
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java
... ... @@ -15,6 +15,7 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO;
15 15 import cn.iocoder.yudao.module.iot.dal.mysql.thingmodel.IotThingModelMapper;
16 16 import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants;
17 17 import cn.iocoder.yudao.module.iot.enums.product.IotProductStatusEnum;
  18 +import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusPointService;
18 19 import cn.iocoder.yudao.module.iot.service.product.IotProductService;
19 20 import jakarta.annotation.Resource;
20 21 import lombok.extern.slf4j.Slf4j;
... ... @@ -50,6 +51,9 @@ public class IotThingModelServiceImpl implements IotThingModelService {
50 51 @Resource
51 52 @Lazy // 延迟加载,解决循环依赖
52 53 private IotProductService productService;
  54 + @Resource
  55 + @Lazy // 延迟加载,解决循环依赖
  56 + private IotDeviceModbusPointService deviceModbusPointService;
53 57  
54 58 @Override
55 59 @Transactional(rollbackFor = Exception.class)
... ... @@ -84,7 +88,11 @@ public class IotThingModelServiceImpl implements IotThingModelService {
84 88 IotThingModelDO thingModel = IotThingModelConvert.INSTANCE.convert(updateReqVO);
85 89 thingModelMapper.updateById(thingModel);
86 90  
87   - // 3. 删除缓存
  91 + // 3. 同步更新 Modbus 点位的冗余字段(identifier、name)
  92 + deviceModbusPointService.updateDeviceModbusPointByThingModel(
  93 + updateReqVO.getId(), updateReqVO.getIdentifier(), updateReqVO.getName());
  94 +
  95 + // 4. 删除缓存
88 96 deleteThingModelListCache(updateReqVO.getProductId());
89 97 }
90 98  
... ...
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleTimerConditionIntegrationTest.java
... ... @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.iot.service.rule.scene;
3 3 import cn.hutool.core.collection.ListUtil;
4 4 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
5 5 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
6   -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
  6 +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum;
7 7 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
8 8 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO;
9 9 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
... ...
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDeviceStateConditionMatcherTest.java
1 1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition;
2 2  
3   -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
  3 +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum;
4 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5 5 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
6 6 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
... ...
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceStateUpdateTriggerMatcherTest.java
1 1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
2 2  
3 3 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
4   -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
  4 +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum;
5 5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
6 6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
7 7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java
1 1 package cn.iocoder.yudao.module.iot.core.biz;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4   -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
5   -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
6   -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
7   -import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
  4 +import cn.iocoder.yudao.module.iot.core.biz.dto.*;
8 5 import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
9 6 import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
10 7 import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
... ... @@ -50,4 +47,12 @@ public interface IotDeviceCommonApi {
50 47 */
51 48 CommonResult<List<IotSubDeviceRegisterRespDTO>> registerSubDevices(IotSubDeviceRegisterFullReqDTO reqDTO);
52 49  
  50 + /**
  51 + * 获取 Modbus 设备配置列表
  52 + *
  53 + * @param listReqDTO 查询参数
  54 + * @return Modbus 设备配置列表
  55 + */
  56 + CommonResult<List<IotModbusDeviceConfigRespDTO>> getModbusDeviceConfigList(IotModbusDeviceConfigListReqDTO listReqDTO);
  57 +
53 58 }
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigListReqDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.biz.dto;
  2 +
  3 +import lombok.Data;
  4 +import lombok.experimental.Accessors;
  5 +
  6 +import java.util.Set;
  7 +
  8 +/**
  9 + * IoT Modbus 设备配置列表查询 Request DTO
  10 + *
  11 + * @author 芋道源码
  12 + */
  13 +@Data
  14 +@Accessors(chain = true)
  15 +public class IotModbusDeviceConfigListReqDTO {
  16 +
  17 + /**
  18 + * 状态
  19 + */
  20 + private Integer status;
  21 +
  22 + /**
  23 + * 模式
  24 + */
  25 + private Integer mode;
  26 +
  27 + /**
  28 + * 协议类型
  29 + */
  30 + private String protocolType;
  31 +
  32 + /**
  33 + * 设备 ID 集合
  34 + */
  35 + private Set<Long> deviceIds;
  36 +
  37 +}
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusDeviceConfigRespDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.biz.dto;
  2 +
  3 +import lombok.Data;
  4 +
  5 +import java.util.List;
  6 +
  7 +/**
  8 + * IoT Modbus 设备配置 Response DTO
  9 + *
  10 + * @author 芋道源码
  11 + */
  12 +@Data
  13 +public class IotModbusDeviceConfigRespDTO {
  14 +
  15 + /**
  16 + * 设备编号
  17 + */
  18 + private Long deviceId;
  19 + /**
  20 + * 产品标识
  21 + */
  22 + private String productKey;
  23 + /**
  24 + * 设备名称
  25 + */
  26 + private String deviceName;
  27 +
  28 + // ========== Modbus 连接配置 ==========
  29 +
  30 + /**
  31 + * Modbus 服务器 IP 地址
  32 + */
  33 + private String ip;
  34 + /**
  35 + * Modbus 服务器端口
  36 + */
  37 + private Integer port;
  38 + /**
  39 + * 从站地址
  40 + */
  41 + private Integer slaveId;
  42 + /**
  43 + * 连接超时时间,单位:毫秒
  44 + */
  45 + private Integer timeout;
  46 + /**
  47 + * 重试间隔,单位:毫秒
  48 + */
  49 + private Integer retryInterval;
  50 + /**
  51 + * 模式
  52 + */
  53 + private Integer mode;
  54 + /**
  55 + * 数据帧格式
  56 + */
  57 + private Integer frameFormat;
  58 +
  59 + // ========== Modbus 点位配置 ==========
  60 +
  61 + /**
  62 + * 点位列表
  63 + */
  64 + private List<IotModbusPointRespDTO> points;
  65 +
  66 +}
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotModbusPointRespDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.biz.dto;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusByteOrderEnum;
  4 +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusRawDataTypeEnum;
  5 +import lombok.Data;
  6 +
  7 +import java.math.BigDecimal;
  8 +
  9 +/**
  10 + * IoT Modbus 点位配置 Response DTO
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +@Data
  15 +public class IotModbusPointRespDTO {
  16 +
  17 + /**
  18 + * 点位编号
  19 + */
  20 + private Long id;
  21 + /**
  22 + * 属性标识符(物模型的 identifier)
  23 + */
  24 + private String identifier;
  25 + /**
  26 + * 属性名称(物模型的 name)
  27 + */
  28 + private String name;
  29 +
  30 + // ========== Modbus 协议配置 ==========
  31 +
  32 + /**
  33 + * Modbus 功能码
  34 + *
  35 + * 取值范围:FC01-04(读线圈、读离散输入、读保持寄存器、读输入寄存器)
  36 + */
  37 + private Integer functionCode;
  38 + /**
  39 + * 寄存器起始地址
  40 + */
  41 + private Integer registerAddress;
  42 + /**
  43 + * 寄存器数量
  44 + */
  45 + private Integer registerCount;
  46 + /**
  47 + * 字节序
  48 + *
  49 + * 枚举 {@link IotModbusByteOrderEnum}
  50 + */
  51 + private String byteOrder;
  52 + /**
  53 + * 原始数据类型
  54 + *
  55 + * 枚举 {@link IotModbusRawDataTypeEnum}
  56 + */
  57 + private String rawDataType;
  58 + /**
  59 + * 缩放因子
  60 + */
  61 + private BigDecimal scale;
  62 + /**
  63 + * 轮询间隔(毫秒)
  64 + */
  65 + private Integer pollInterval;
  66 +
  67 +}
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageMethodEnum.java
... ... @@ -64,7 +64,7 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable&lt;String&gt; {
64 64 // ========== OTA 固件 ==========
65 65 // 可参考:https://help.aliyun.com/zh/iot/user-guide/perform-ota-updates
66 66  
67   - OTA_UPGRADE("thing.ota.upgrade", "OTA 固定信息推送", false),
  67 + OTA_UPGRADE("thing.ota.upgrade", "OTA 固件信息推送", false),
68 68 OTA_PROGRESS("thing.ota.progress", "OTA 升级进度上报", true),
69 69  
70 70 ;
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotProtocolTypeEnum.java
... ... @@ -25,7 +25,8 @@ public enum IotProtocolTypeEnum implements ArrayValuable&lt;String&gt; {
25 25 MQTT("mqtt"),
26 26 EMQX("emqx"),
27 27 COAP("coap"),
28   - MODBUS_TCP("modbus_tcp");
  28 + MODBUS_TCP_CLIENT("modbus_tcp_client"),
  29 + MODBUS_TCP_SERVER("modbus_tcp_server");
29 30  
30 31 public static final String[] ARRAYS = Arrays.stream(values()).map(IotProtocolTypeEnum::getType).toArray(String[]::new);
31 32  
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceStateEnum.java renamed to yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/device/IotDeviceStateEnum.java
1   -package cn.iocoder.yudao.module.iot.core.enums;
  1 +package cn.iocoder.yudao.module.iot.core.enums.device;
2 2  
3 3 import cn.iocoder.yudao.framework.common.core.ArrayValuable;
4 4 import lombok.Getter;
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/modbus/IotModbusByteOrderEnum.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.enums.modbus;
  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 IotModbusByteOrderEnum implements ArrayValuable<String> {
  17 +
  18 + AB("AB", "大端序(16位)", 2),
  19 + BA("BA", "小端序(16位)", 2),
  20 + ABCD("ABCD", "大端序(32位)", 4),
  21 + CDAB("CDAB", "大端字交换(32位)", 4),
  22 + DCBA("DCBA", "小端序(32位)", 4),
  23 + BADC("BADC", "小端字交换(32位)", 4);
  24 +
  25 + public static final String[] ARRAYS = Arrays.stream(values())
  26 + .map(IotModbusByteOrderEnum::getOrder)
  27 + .toArray(String[]::new);
  28 +
  29 + /**
  30 + * 字节序
  31 + */
  32 + private final String order;
  33 + /**
  34 + * 名称
  35 + */
  36 + private final String name;
  37 + /**
  38 + * 字节数
  39 + */
  40 + private final Integer byteCount;
  41 +
  42 + @Override
  43 + public String[] array() {
  44 + return ARRAYS;
  45 + }
  46 +
  47 + public static IotModbusByteOrderEnum getByOrder(String order) {
  48 + return Arrays.stream(values())
  49 + .filter(e -> e.getOrder().equals(order))
  50 + .findFirst()
  51 + .orElse(null);
  52 + }
  53 +
  54 +}
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/modbus/IotModbusFrameFormatEnum.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.enums.modbus;
  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 IotModbusFrameFormatEnum implements ArrayValuable<Integer> {
  17 +
  18 + MODBUS_TCP(1),
  19 + MODBUS_RTU(2);
  20 +
  21 + public static final Integer[] ARRAYS = Arrays.stream(values())
  22 + .map(IotModbusFrameFormatEnum::getFormat)
  23 + .toArray(Integer[]::new);
  24 +
  25 + /**
  26 + * 格式
  27 + */
  28 + private final Integer format;
  29 +
  30 + @Override
  31 + public Integer[] array() {
  32 + return ARRAYS;
  33 + }
  34 +
  35 +}
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/modbus/IotModbusModeEnum.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.enums.modbus;
  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 IotModbusModeEnum implements ArrayValuable<Integer> {
  17 +
  18 + POLLING(1, "云端轮询"),
  19 + ACTIVE_REPORT(2, "边缘采集");
  20 +
  21 + public static final Integer[] ARRAYS = Arrays.stream(values())
  22 + .map(IotModbusModeEnum::getMode)
  23 + .toArray(Integer[]::new);
  24 +
  25 + /**
  26 + * 工作模式
  27 + */
  28 + private final Integer mode;
  29 + /**
  30 + * 模式名称
  31 + */
  32 + private final String name;
  33 +
  34 + @Override
  35 + public Integer[] array() {
  36 + return ARRAYS;
  37 + }
  38 +
  39 +}
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/modbus/IotModbusRawDataTypeEnum.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.enums.modbus;
  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 IotModbusRawDataTypeEnum implements ArrayValuable<String> {
  17 +
  18 + INT16("INT16", "有符号 16 位整数", 1),
  19 + UINT16("UINT16", "无符号 16 位整数", 1),
  20 + INT32("INT32", "有符号 32 位整数", 2),
  21 + UINT32("UINT32", "无符号 32 位整数", 2),
  22 + FLOAT("FLOAT", "32 位浮点数", 2),
  23 + DOUBLE("DOUBLE", "64 位浮点数", 4),
  24 + BOOLEAN("BOOLEAN", "布尔值(用于线圈)", 1),
  25 + STRING("STRING", "字符串", null); // null 表示可变长度
  26 +
  27 + public static final String[] ARRAYS = Arrays.stream(values())
  28 + .map(IotModbusRawDataTypeEnum::getType)
  29 + .toArray(String[]::new);
  30 +
  31 + /**
  32 + * 数据类型
  33 + */
  34 + private final String type;
  35 + /**
  36 + * 名称
  37 + */
  38 + private final String name;
  39 + /**
  40 + * 寄存器数量(null 表示可变)
  41 + */
  42 + private final Integer registerCount;
  43 +
  44 + @Override
  45 + public String[] array() {
  46 + return ARRAYS;
  47 + }
  48 +
  49 + public static IotModbusRawDataTypeEnum getByType(String type) {
  50 + return Arrays.stream(values())
  51 + .filter(e -> e.getType().equals(type))
  52 + .findFirst()
  53 + .orElse(null);
  54 + }
  55 +
  56 +}
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java
1 1 package cn.iocoder.yudao.module.iot.core.mq.message;
2 2  
3   -import cn.hutool.core.map.MapUtil;
4 3 import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
5 4 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
6   -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
  5 +import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum;
  6 +import cn.iocoder.yudao.module.iot.core.topic.state.IotDeviceStateUpdateReqDTO;
7 7 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
8 8 import lombok.AllArgsConstructor;
9 9 import lombok.Builder;
... ... @@ -72,7 +72,7 @@ public class IotDeviceMessage {
72 72 * 请求方法
73 73 *
74 74 * 枚举 {@link IotDeviceMessageMethodEnum}
75   - * 例如说:thing.property.report 属性上报
  75 + * 例如说:thing.property.post 属性上报
76 76 */
77 77 private String method;
78 78 /**
... ... @@ -149,20 +149,12 @@ public class IotDeviceMessage {
149 149  
150 150 public static IotDeviceMessage buildStateUpdateOnline() {
151 151 return requestOf(IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod(),
152   - MapUtil.of("state", IotDeviceStateEnum.ONLINE.getState()));
  152 + new IotDeviceStateUpdateReqDTO(IotDeviceStateEnum.ONLINE.getState()));
153 153 }
154 154  
155 155 public static IotDeviceMessage buildStateOffline() {
156 156 return requestOf(IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod(),
157   - MapUtil.of("state", IotDeviceStateEnum.OFFLINE.getState()));
158   - }
159   -
160   - public static IotDeviceMessage buildOtaUpgrade(String version, String fileUrl, Long fileSize,
161   - String fileDigestAlgorithm, String fileDigestValue) {
162   - return requestOf(IotDeviceMessageMethodEnum.OTA_UPGRADE.getMethod(), MapUtil.builder()
163   - .put("version", version).put("fileUrl", fileUrl).put("fileSize", fileSize)
164   - .put("fileDigestAlgorithm", fileDigestAlgorithm).put("fileDigestValue", fileDigestValue)
165   - .build());
  157 + new IotDeviceStateUpdateReqDTO(IotDeviceStateEnum.OFFLINE.getState()));
166 158 }
167 159  
168 160 }
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java
1 1 package cn.iocoder.yudao.module.iot.core.topic.auth;
2 2  
  3 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
3 4 import jakarta.validation.constraints.NotEmpty;
4 5 import lombok.Data;
5 6  
6 7 /**
7 8 * IoT 设备动态注册 Request DTO
8 9 * <p>
9   - * 用于直连设备/网关的一型一密动态注册:使用 productSecret 验证,返回 deviceSecret
  10 + * 用于 {@link IotDeviceMessageMethodEnum#DEVICE_REGISTER} 消息的 params 参数
  11 + * <p>
  12 + * 直连设备/网关的一型一密动态注册:使用 productSecret 验证,返回 deviceSecret
10 13 *
11 14 * @author 芋道源码
12 15 * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterRespDTO.java
1 1 package cn.iocoder.yudao.module.iot.core.topic.auth;
2 2  
  3 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
3 4 import lombok.AllArgsConstructor;
4 5 import lombok.Data;
5 6 import lombok.NoArgsConstructor;
... ... @@ -7,7 +8,7 @@ import lombok.NoArgsConstructor;
7 8 /**
8 9 * IoT 设备动态注册 Response DTO
9 10 * <p>
10   - * 用于直连设备/网关的一型一密动态注册响应
  11 + * 用于 {@link IotDeviceMessageMethodEnum#DEVICE_REGISTER} 响应的设备信息
11 12 *
12 13 * @author 芋道源码
13 14 * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterReqDTO.java
1 1 package cn.iocoder.yudao.module.iot.core.topic.auth;
2 2  
  3 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
3 4 import jakarta.validation.constraints.NotEmpty;
4 5 import lombok.Data;
5 6  
6 7 /**
7 8 * IoT 子设备动态注册 Request DTO
8 9 * <p>
9   - * 用于 thing.auth.register.sub 消息的 params 数组元素
10   - *
  10 + * 用于 {@link IotDeviceMessageMethodEnum#SUB_DEVICE_REGISTER} 消息的 params 数组元素
  11 + * <p>
11 12 * 特殊:网关子设备的动态注册,必须已经创建好该网关子设备(不然哪来的 {@link #deviceName} 字段)。更多的好处,是设备不用提前烧录 deviceSecret 密钥。
12 13 *
13 14 * @author 芋道源码
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterRespDTO.java
1 1 package cn.iocoder.yudao.module.iot.core.topic.auth;
2 2  
  3 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
3 4 import lombok.AllArgsConstructor;
4 5 import lombok.Data;
5 6 import lombok.NoArgsConstructor;
... ... @@ -7,7 +8,7 @@ import lombok.NoArgsConstructor;
7 8 /**
8 9 * IoT 子设备动态注册 Response DTO
9 10 * <p>
10   - * 用于 thing.auth.register.sub 响应的设备信息
  11 + * 用于 {@link IotDeviceMessageMethodEnum#SUB_DEVICE_REGISTER} 响应的设备信息
11 12 *
12 13 * @author 芋道源码
13 14 * @see <a href="https://help.aliyun.com/zh/iot/user-guide/register-devices">阿里云 - 动态注册子设备</a>
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/config/IotDeviceConfigPushReqDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.topic.config;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
  4 +import lombok.AllArgsConstructor;
  5 +import lombok.Data;
  6 +import lombok.NoArgsConstructor;
  7 +
  8 +/**
  9 + * IoT 设备配置推送 Request DTO
  10 + * <p>
  11 + * 用于 {@link IotDeviceMessageMethodEnum#CONFIG_PUSH} 下行消息的 params 参数
  12 + *
  13 + * @author 芋道源码
  14 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/remote-configuration-1">阿里云 - 远程配置</a>
  15 + */
  16 +@Data
  17 +@NoArgsConstructor
  18 +@AllArgsConstructor
  19 +public class IotDeviceConfigPushReqDTO {
  20 +
  21 + /**
  22 + * 配置编号
  23 + */
  24 + private String configId;
  25 +
  26 + /**
  27 + * 配置文件大小(字节)
  28 + */
  29 + private Long configSize;
  30 +
  31 + /**
  32 + * 签名方法
  33 + */
  34 + private String signMethod;
  35 +
  36 + /**
  37 + * 签名
  38 + */
  39 + private String sign;
  40 +
  41 + /**
  42 + * 配置文件下载地址
  43 + */
  44 + private String url;
  45 +
  46 + /**
  47 + * 获取类型
  48 + * <p>
  49 + * file: 文件
  50 + * content: 内容
  51 + */
  52 + private String getType;
  53 +
  54 +}
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/event/IotDeviceEventPostReqDTO.java
1 1 package cn.iocoder.yudao.module.iot.core.topic.event;
2 2  
  3 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
3 4 import lombok.Data;
4 5  
5 6 /**
6 7 * IoT 设备事件上报 Request DTO
7 8 * <p>
8   - * 用于 thing.event.post 消息的 params 参数
  9 + * 用于 {@link IotDeviceMessageMethodEnum#EVENT_POST} 消息的 params 参数
9 10 *
10 11 * @author 芋道源码
11 12 * @see <a href="http://help.aliyun.com/zh/marketplace/device-reporting-events">阿里云 - 设备上报事件</a>
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/ota/IotDeviceOtaProgressReqDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.topic.ota;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
  4 +import lombok.AllArgsConstructor;
  5 +import lombok.Data;
  6 +import lombok.NoArgsConstructor;
  7 +
  8 +/**
  9 + * IoT 设备 OTA 升级进度上报 Request DTO
  10 + * <p>
  11 + * 用于 {@link IotDeviceMessageMethodEnum#OTA_PROGRESS} 上行消息的 params 参数
  12 + *
  13 + * @author 芋道源码
  14 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/perform-ota-updates">阿里云 - OTA 升级</a>
  15 + */
  16 +@Data
  17 +@NoArgsConstructor
  18 +@AllArgsConstructor
  19 +public class IotDeviceOtaProgressReqDTO {
  20 +
  21 + /**
  22 + * 固件版本号
  23 + */
  24 + private String version;
  25 +
  26 + /**
  27 + * 升级状态
  28 + */
  29 + private Integer status;
  30 +
  31 + /**
  32 + * 描述信息
  33 + */
  34 + private String description;
  35 +
  36 + /**
  37 + * 升级进度(0-100)
  38 + */
  39 + private Integer progress;
  40 +
  41 +}
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/ota/IotDeviceOtaUpgradeReqDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.topic.ota;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
  4 +import lombok.AllArgsConstructor;
  5 +import lombok.Data;
  6 +import lombok.NoArgsConstructor;
  7 +
  8 +/**
  9 + * IoT 设备 OTA 固件升级推送 Request DTO
  10 + * <p>
  11 + * 用于 {@link IotDeviceMessageMethodEnum#OTA_UPGRADE} 下行消息的 params 参数
  12 + *
  13 + * @author 芋道源码
  14 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/perform-ota-updates">阿里云 - OTA 升级</a>
  15 + */
  16 +@Data
  17 +@NoArgsConstructor
  18 +@AllArgsConstructor
  19 +public class IotDeviceOtaUpgradeReqDTO {
  20 +
  21 + /**
  22 + * 固件版本号
  23 + */
  24 + private String version;
  25 +
  26 + /**
  27 + * 固件文件下载地址
  28 + */
  29 + private String fileUrl;
  30 +
  31 + /**
  32 + * 固件文件大小(字节)
  33 + */
  34 + private Long fileSize;
  35 +
  36 + /**
  37 + * 固件文件摘要算法
  38 + */
  39 + private String fileDigestAlgorithm;
  40 +
  41 + /**
  42 + * 固件文件摘要值
  43 + */
  44 + private String fileDigestValue;
  45 +
  46 +}
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPackPostReqDTO.java
1 1 package cn.iocoder.yudao.module.iot.core.topic.property;
2 2  
  3 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
3 4 import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
4 5 import lombok.Data;
5 6  
... ... @@ -9,7 +10,7 @@ import java.util.Map;
9 10 /**
10 11 * IoT 设备属性批量上报 Request DTO
11 12 * <p>
12   - * 用于 thing.event.property.pack.post 消息的 params 参数
  13 + * 用于 {@link IotDeviceMessageMethodEnum#PROPERTY_PACK_POST} 消息的 params 参数
13 14 *
14 15 * @author 芋道源码
15 16 * @see <a href="http://help.aliyun.com/zh/marketplace/gateway-reports-data-in-batches">阿里云 - 网关批量上报数据</a>
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPostReqDTO.java
1 1 package cn.iocoder.yudao.module.iot.core.topic.property;
2 2  
  3 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
  4 +
3 5 import java.util.HashMap;
4 6 import java.util.Map;
5 7  
6 8 /**
7 9 * IoT 设备属性上报 Request DTO
8 10 * <p>
9   - * 用于 thing.property.post 消息的 params 参数
  11 + * 用于 {@link IotDeviceMessageMethodEnum#PROPERTY_POST} 消息的 params 参数
10 12 * <p>
11 13 * 本质是一个 Map,key 为属性标识符,value 为属性值
12 14 *
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertySetReqDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.topic.property;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
  4 +
  5 +import java.util.HashMap;
  6 +import java.util.Map;
  7 +
  8 +/**
  9 + * IoT 设备属性设置 Request DTO
  10 + * <p>
  11 + * 用于 {@link IotDeviceMessageMethodEnum#PROPERTY_SET} 下行消息的 params 参数
  12 + * <p>
  13 + * 本质是一个 Map,key 为属性标识符,value 为属性值
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +public class IotDevicePropertySetReqDTO extends HashMap<String, Object> {
  18 +
  19 + public IotDevicePropertySetReqDTO() {
  20 + super();
  21 + }
  22 +
  23 + public IotDevicePropertySetReqDTO(Map<String, Object> properties) {
  24 + super(properties);
  25 + }
  26 +
  27 + /**
  28 + * 创建属性设置 DTO
  29 + *
  30 + * @param properties 属性数据
  31 + * @return DTO 对象
  32 + */
  33 + public static IotDevicePropertySetReqDTO of(Map<String, Object> properties) {
  34 + return new IotDevicePropertySetReqDTO(properties);
  35 + }
  36 +
  37 +}
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/service/IotDeviceServiceInvokeReqDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.topic.service;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
  4 +import lombok.AllArgsConstructor;
  5 +import lombok.Data;
  6 +import lombok.NoArgsConstructor;
  7 +
  8 +import java.util.Map;
  9 +
  10 +/**
  11 + * IoT 设备服务调用 Request DTO
  12 + * <p>
  13 + * 用于 {@link IotDeviceMessageMethodEnum#SERVICE_INVOKE} 下行消息的 params 参数
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Data
  18 +@NoArgsConstructor
  19 +@AllArgsConstructor
  20 +public class IotDeviceServiceInvokeReqDTO {
  21 +
  22 + /**
  23 + * 服务标识符
  24 + */
  25 + private String identifier;
  26 +
  27 + /**
  28 + * 服务输入参数
  29 + */
  30 + private Map<String, Object> inputParams;
  31 +
  32 +}
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/state/IotDeviceStateUpdateReqDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.topic.state;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
  4 +import lombok.AllArgsConstructor;
  5 +import lombok.Data;
  6 +import lombok.NoArgsConstructor;
  7 +
  8 +/**
  9 + * IoT 设备状态更新 Request DTO
  10 + * <p>
  11 + * 用于 {@link IotDeviceMessageMethodEnum#STATE_UPDATE} 消息的 params 参数
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +@Data
  16 +@NoArgsConstructor
  17 +@AllArgsConstructor
  18 +public class IotDeviceStateUpdateReqDTO {
  19 +
  20 + /**
  21 + * 设备状态
  22 + */
  23 + private Integer state;
  24 +
  25 +}
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoAddReqDTO.java
1 1 package cn.iocoder.yudao.module.iot.core.topic.topo;
2 2  
3 3 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
  4 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
4 5 import jakarta.validation.constraints.NotEmpty;
5 6 import lombok.Data;
6 7  
... ... @@ -9,7 +10,7 @@ import java.util.List;
9 10 /**
10 11 * IoT 设备拓扑添加 Request DTO
11 12 * <p>
12   - * 用于 thing.topo.add 消息的 params 参数
  13 + * 用于 {@link IotDeviceMessageMethodEnum#TOPO_ADD} 消息的 params 参数
13 14 *
14 15 * @author 芋道源码
15 16 * @see <a href="http://help.aliyun.com/zh/marketplace/add-topological-relationship">阿里云 - 添加拓扑关系</a>
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoChangeReqDTO.java
1 1 package cn.iocoder.yudao.module.iot.core.topic.topo;
2 2  
  3 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
3 4 import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
4 5 import lombok.AllArgsConstructor;
5 6 import lombok.Data;
... ... @@ -10,7 +11,7 @@ import java.util.List;
10 11 /**
11 12 * IoT 设备拓扑关系变更通知 Request DTO
12 13 * <p>
13   - * 用于 thing.topo.change 下行消息的 params 参数
  14 + * 用于 {@link IotDeviceMessageMethodEnum#TOPO_CHANGE} 下行消息的 params 参数
14 15 *
15 16 * @author 芋道源码
16 17 * @see <a href="https://help.aliyun.com/zh/marketplace/notify-gateway-topology-changes">阿里云 - 通知网关拓扑关系变化</a>
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoDeleteReqDTO.java
1 1 package cn.iocoder.yudao.module.iot.core.topic.topo;
2 2  
  3 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
3 4 import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
4 5 import jakarta.validation.Valid;
5 6 import jakarta.validation.constraints.NotEmpty;
... ... @@ -10,7 +11,7 @@ import java.util.List;
10 11 /**
11 12 * IoT 设备拓扑删除 Request DTO
12 13 * <p>
13   - * 用于 thing.topo.delete 消息的 params 参数
  14 + * 用于 {@link IotDeviceMessageMethodEnum#TOPO_DELETE} 消息的 params 参数
14 15 *
15 16 * @author 芋道源码
16 17 * @see <a href="https://help.aliyun.com/zh/marketplace/delete-a-topological-relationship">阿里云 - 删除拓扑关系</a>
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetReqDTO.java
1 1 package cn.iocoder.yudao.module.iot.core.topic.topo;
2 2  
  3 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
3 4 import lombok.Data;
4 5  
5 6 /**
6 7 * IoT 设备拓扑关系获取 Request DTO
7 8 * <p>
8   - * 用于 thing.topo.get 请求的 params 参数(目前为空,预留扩展)
  9 + * 用于 {@link IotDeviceMessageMethodEnum#TOPO_GET} 请求的 params 参数(目前为空,预留扩展)
9 10 *
10 11 * @author 芋道源码
11 12 * @see <a href="https://help.aliyun.com/zh/marketplace/obtain-topological-relationship">阿里云 - 获取拓扑关系</a>
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetRespDTO.java
1 1 package cn.iocoder.yudao.module.iot.core.topic.topo;
2 2  
  3 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
3 4 import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
4 5 import lombok.Data;
5 6  
... ... @@ -8,7 +9,7 @@ import java.util.List;
8 9 /**
9 10 * IoT 设备拓扑关系获取 Response DTO
10 11 * <p>
11   - * 用于 thing.topo.get 响应
  12 + * 用于 {@link IotDeviceMessageMethodEnum#TOPO_GET} 响应
12 13 *
13 14 * @author 芋道源码
14 15 * @see <a href="https://help.aliyun.com/zh/marketplace/obtain-topological-relationship">阿里云 - 获取拓扑关系</a>
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java
... ... @@ -25,6 +25,14 @@ public class IotDeviceAuthUtils {
25 25 return String.format("%s.%s", productKey, deviceName);
26 26 }
27 27  
  28 + public static String buildClientIdFromUsername(String username) {
  29 + IotDeviceIdentity identity = parseUsername(username);
  30 + if (identity == null) {
  31 + return null;
  32 + }
  33 + return buildClientId(identity.getProductKey(), identity.getDeviceName());
  34 + }
  35 +
28 36 public static String buildUsername(String productKey, String deviceName) {
29 37 return String.format("%s&%s", deviceName, productKey);
30 38 }
... ...
yudao-module-iot/yudao-module-iot-gateway/pom.xml
... ... @@ -33,7 +33,7 @@
33 33 <groupId>org.apache.rocketmq</groupId>
34 34 <artifactId>rocketmq-spring-boot-starter</artifactId>
35 35 <!-- TODO @芋艿:消息队列,后续可能去掉,默认不使用 rocketmq -->
36   -<!-- <optional>true</optional> -->
  36 + <optional>true</optional>
37 37 </dependency>
38 38  
39 39 <!-- 工具类相关 -->
... ... @@ -48,6 +48,13 @@
48 48 <artifactId>vertx-mqtt</artifactId>
49 49 </dependency>
50 50  
  51 + <!-- Modbus 相关 -->
  52 + <dependency>
  53 + <groupId>com.ghgande</groupId>
  54 + <artifactId>j2mod</artifactId>
  55 + <version>3.2.1</version>
  56 + </dependency>
  57 +
51 58 <!-- CoAP 相关 - Eclipse Californium -->
52 59 <dependency>
53 60 <groupId>org.eclipse.californium</groupId>
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java
... ... @@ -6,6 +6,11 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
6 6 import org.springframework.context.annotation.Bean;
7 7 import org.springframework.context.annotation.Configuration;
8 8  
  9 +/**
  10 + * IoT 网关配置类
  11 + *
  12 + * @author 芋道源码
  13 + */
9 14 @Configuration
10 15 @EnableConfigurationProperties(IotGatewayProperties.class)
11 16 public class IotGatewayConfiguration {
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java
... ... @@ -4,6 +4,8 @@ import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
4 4 import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapConfig;
5 5 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxConfig;
6 6 import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpConfig;
  7 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.IotModbusTcpClientConfig;
  8 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.IotModbusTcpServerConfig;
7 9 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttConfig;
8 10 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig;
9 11 import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpConfig;
... ... @@ -166,6 +168,18 @@ public class IotGatewayProperties {
166 168 @Valid
167 169 private IotEmqxConfig emqx;
168 170  
  171 + /**
  172 + * Modbus TCP Client 协议配置
  173 + */
  174 + @Valid
  175 + private IotModbusTcpClientConfig modbusTcpClient;
  176 +
  177 + /**
  178 + * Modbus TCP Server 协议配置
  179 + */
  180 + @Valid
  181 + private IotModbusTcpServerConfig modbusTcpServer;
  182 +
169 183 }
170 184  
171 185 /**
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolDownstreamSubscriber.java renamed to yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/AbstractIotProtocolDownstreamSubscriber.java
... ... @@ -17,7 +17,7 @@ import lombok.extern.slf4j.Slf4j;
17 17 */
18 18 @AllArgsConstructor
19 19 @Slf4j
20   -public abstract class IotProtocolDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
  20 +public abstract class AbstractIotProtocolDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
21 21  
22 22 private final IotProtocol protocol;
23 23  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/IotProtocolManager.java
... ... @@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
7 7 import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol;
8 8 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol;
9 9 import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol;
  10 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.IotModbusTcpClientProtocol;
  11 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.IotModbusTcpServerProtocol;
10 12 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol;
11 13 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpProtocol;
12 14 import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol;
... ... @@ -112,6 +114,10 @@ public class IotProtocolManager implements SmartLifecycle {
112 114 return createMqttProtocol(config);
113 115 case EMQX:
114 116 return createEmqxProtocol(config);
  117 + case MODBUS_TCP_CLIENT:
  118 + return createModbusTcpClientProtocol(config);
  119 + case MODBUS_TCP_SERVER:
  120 + return createModbusTcpServerProtocol(config);
115 121 default:
116 122 throw new IllegalArgumentException(String.format(
117 123 "[createProtocol][协议实例 %s 的协议类型 %s 暂不支持]", config.getId(), protocolType));
... ... @@ -188,4 +194,24 @@ public class IotProtocolManager implements SmartLifecycle {
188 194 return new IotEmqxProtocol(config);
189 195 }
190 196  
  197 + /**
  198 + * 创建 Modbus TCP Client 协议实例
  199 + *
  200 + * @param config 协议实例配置
  201 + * @return Modbus TCP Client 协议实例
  202 + */
  203 + private IotModbusTcpClientProtocol createModbusTcpClientProtocol(IotGatewayProperties.ProtocolProperties config) {
  204 + return new IotModbusTcpClientProtocol(config);
  205 + }
  206 +
  207 + /**
  208 + * 创建 Modbus TCP Server 协议实例
  209 + *
  210 + * @param config 协议实例配置
  211 + * @return Modbus TCP Server 协议实例
  212 + */
  213 + private IotModbusTcpServerProtocol createModbusTcpServerProtocol(IotGatewayProperties.ProtocolProperties config) {
  214 + return new IotModbusTcpServerProtocol(config);
  215 + }
  216 +
191 217 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapProtocol.java
... ... @@ -64,17 +64,13 @@ public class IotCoapProtocol implements IotProtocol {
64 64 /**
65 65 * 下行消息订阅者
66 66 */
67   - private final IotCoapDownstreamSubscriber downstreamSubscriber;
  67 + private IotCoapDownstreamSubscriber downstreamSubscriber;
68 68  
69 69 public IotCoapProtocol(ProtocolProperties properties) {
70 70 IotCoapConfig coapConfig = properties.getCoap();
71 71 Assert.notNull(coapConfig, "CoAP 协议配置(coap)不能为空");
72 72 this.properties = properties;
73 73 this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
74   -
75   - // 初始化下行消息订阅者
76   - IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
77   - this.downstreamSubscriber = new IotCoapDownstreamSubscriber(this, messageBus);
78 74 }
79 75  
80 76 @Override
... ... @@ -94,9 +90,9 @@ public class IotCoapProtocol implements IotProtocol {
94 90 return;
95 91 }
96 92  
97   - IotCoapConfig coapConfig = properties.getCoap();
98 93 try {
99 94 // 1.1 创建 CoAP 配置
  95 + IotCoapConfig coapConfig = properties.getCoap();
100 96 Configuration config = Configuration.createStandardWithoutFile();
101 97 config.set(CoapConfig.COAP_PORT, properties.getPort());
102 98 config.set(CoapConfig.MAX_MESSAGE_SIZE, coapConfig.getMaxMessageSize());
... ... @@ -131,13 +127,12 @@ public class IotCoapProtocol implements IotProtocol {
131 127 getId(), properties.getPort(), serverId);
132 128  
133 129 // 4. 启动下行消息订阅者
  130 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
  131 + this.downstreamSubscriber = new IotCoapDownstreamSubscriber(this, messageBus);
134 132 this.downstreamSubscriber.start();
135 133 } catch (Exception e) {
136 134 log.error("[start][IoT CoAP 协议 {} 启动失败]", getId(), e);
137   - if (coapServer != null) {
138   - coapServer.destroy();
139   - coapServer = null;
140   - }
  135 + stop0();
141 136 throw e;
142 137 }
143 138 }
... ... @@ -147,12 +142,19 @@ public class IotCoapProtocol implements IotProtocol {
147 142 if (!running) {
148 143 return;
149 144 }
  145 + stop0();
  146 + }
  147 +
  148 + private void stop0() {
150 149 // 1. 停止下行消息订阅者
151   - try {
152   - downstreamSubscriber.stop();
153   - log.info("[stop][IoT CoAP 协议 {} 下行消息订阅者已停止]", getId());
154   - } catch (Exception e) {
155   - log.error("[stop][IoT CoAP 协议 {} 下行消息订阅者停止失败]", getId(), e);
  150 + if (downstreamSubscriber != null) {
  151 + try {
  152 + downstreamSubscriber.stop();
  153 + log.info("[stop][IoT CoAP 协议 {} 下行消息订阅者已停止]", getId());
  154 + } catch (Exception e) {
  155 + log.error("[stop][IoT CoAP 协议 {} 下行消息订阅者停止失败]", getId(), e);
  156 + }
  157 + downstreamSubscriber = null;
156 158 }
157 159  
158 160 // 2. 关闭 CoAP 服务器
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/handler/downstream/IotCoapDownstreamSubscriber.java
... ... @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.downstream;
2 2  
3 3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5   -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber;
6 6 import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol;
7 7 import lombok.extern.slf4j.Slf4j;
8 8  
... ... @@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j;
12 12 * @author 芋道源码
13 13 */
14 14 @Slf4j
15   -public class IotCoapDownstreamSubscriber extends IotProtocolDownstreamSubscriber {
  15 +public class IotCoapDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
16 16  
17 17 public IotCoapDownstreamSubscriber(IotCoapProtocol protocol, IotMessageBus messageBus) {
18 18 super(protocol, messageBus);
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxProtocol.java
... ... @@ -90,7 +90,7 @@ public class IotEmqxProtocol implements IotProtocol {
90 90 /**
91 91 * 下行消息订阅者
92 92 */
93   - private final IotEmqxDownstreamSubscriber downstreamSubscriber;
  93 + private IotEmqxDownstreamSubscriber downstreamSubscriber;
94 94  
95 95 public IotEmqxProtocol(ProtocolProperties properties) {
96 96 Assert.notNull(properties, "协议实例配置不能为空");
... ... @@ -101,10 +101,6 @@ public class IotEmqxProtocol implements IotProtocol {
101 101 "MQTT 连接超时时间(emqx.connect-timeout-seconds)不能为空");
102 102 this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
103 103 this.upstreamHandler = new IotEmqxUpstreamHandler(serverId);
104   -
105   - // 初始化下行消息订阅者
106   - IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
107   - this.downstreamSubscriber = new IotEmqxDownstreamSubscriber(this, messageBus);
108 104 }
109 105  
110 106 @Override
... ... @@ -124,7 +120,7 @@ public class IotEmqxProtocol implements IotProtocol {
124 120 return;
125 121 }
126 122  
127   - // 1.1 创建 Vertx 实例
  123 + // 1.1 创建 Vertx 实例 和 下行消息订阅者
128 124 this.vertx = Vertx.vertx();
129 125  
130 126 try {
... ... @@ -138,6 +134,8 @@ public class IotEmqxProtocol implements IotProtocol {
138 134 getId(), properties.getPort(), serverId);
139 135  
140 136 // 2. 启动下行消息订阅者
  137 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
  138 + this.downstreamSubscriber = new IotEmqxDownstreamSubscriber(this, messageBus);
141 139 this.downstreamSubscriber.start();
142 140 } catch (Exception e) {
143 141 log.error("[start][IoT EMQX 协议 {} 启动失败]", getId(), e);
... ... @@ -157,11 +155,14 @@ public class IotEmqxProtocol implements IotProtocol {
157 155  
158 156 private void stop0() {
159 157 // 1. 停止下行消息订阅者
160   - try {
161   - downstreamSubscriber.stop();
162   - log.info("[stop][IoT EMQX 协议 {} 下行消息订阅者已停止]", getId());
163   - } catch (Exception e) {
164   - log.error("[stop][IoT EMQX 协议 {} 下行消息订阅者停止失败]", getId(), e);
  158 + if (downstreamSubscriber != null) {
  159 + try {
  160 + downstreamSubscriber.stop();
  161 + log.info("[stop][IoT EMQX 协议 {} 下行消息订阅者已停止]", getId());
  162 + } catch (Exception e) {
  163 + log.error("[stop][IoT EMQX 协议 {} 下行消息订阅者停止失败]", getId(), e);
  164 + }
  165 + downstreamSubscriber = null;
165 166 }
166 167  
167 168 // 2.1 先置为 false:避免 closeHandler 触发重连
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamSubscriber.java
... ... @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream;
2 2  
3 3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5   -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber;
6 6 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol;
7 7 import lombok.extern.slf4j.Slf4j;
8 8  
... ... @@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j;
12 12 * @author 芋道源码
13 13 */
14 14 @Slf4j
15   -public class IotEmqxDownstreamSubscriber extends IotProtocolDownstreamSubscriber {
  15 +public class IotEmqxDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
16 16  
17 17 private final IotEmqxDownstreamHandler downstreamHandler;
18 18  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxUpstreamHandler.java
... ... @@ -5,6 +5,7 @@ import cn.hutool.core.util.StrUtil;
5 5 import cn.hutool.extra.spring.SpringUtil;
6 6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7 7 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  8 +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
8 9 import io.vertx.mqtt.messages.MqttPublishMessage;
9 10 import lombok.extern.slf4j.Slf4j;
10 11  
... ... @@ -42,12 +43,14 @@ public class IotEmqxUpstreamHandler {
42 43 return;
43 44 }
44 45  
45   - // 2. 反序列化消息
  46 + // 2.1 反序列化消息
46 47 IotDeviceMessage message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName);
47 48 if (message == null) {
48 49 log.warn("[handle][topic({}) payload({}) 消息解码失败]", topic, new String(payload));
49 50 return;
50 51 }
  52 + // 2.2 标准化回复消息的 method(MQTT 协议中,设备回复消息的 method 会携带 _reply 后缀)
  53 + IotMqttTopicUtils.normalizeReplyMethod(message);
51 54  
52 55 // 3. 发送消息到队列
53 56 deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId);
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java
... ... @@ -64,10 +64,6 @@ public class IotHttpProtocol implements IotProtocol {
64 64 public IotHttpProtocol(ProtocolProperties properties) {
65 65 this.properties = properties;
66 66 this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
67   -
68   - // 初始化下行消息订阅者
69   - IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
70   - this.downstreamSubscriber = new IotHttpDownstreamSubscriber(this, messageBus);
71 67 }
72 68  
73 69 @Override
... ... @@ -87,7 +83,7 @@ public class IotHttpProtocol implements IotProtocol {
87 83 return;
88 84 }
89 85  
90   - // 1.1 创建 Vertx 实例(每个 Protocol 独立管理)
  86 + // 1.1 创建 Vertx 实例
91 87 this.vertx = Vertx.vertx();
92 88  
93 89 // 1.2 创建路由
... ... @@ -123,18 +119,12 @@ public class IotHttpProtocol implements IotProtocol {
123 119 getId(), properties.getPort(), serverId);
124 120  
125 121 // 2. 启动下行消息订阅者
  122 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
  123 + this.downstreamSubscriber = new IotHttpDownstreamSubscriber(this, messageBus);
126 124 this.downstreamSubscriber.start();
127 125 } catch (Exception e) {
128 126 log.error("[start][IoT HTTP 协议 {} 启动失败]", getId(), e);
129   - // 启动失败时关闭资源
130   - if (httpServer != null) {
131   - httpServer.close();
132   - httpServer = null;
133   - }
134   - if (vertx != null) {
135   - vertx.close();
136   - vertx = null;
137   - }
  127 + stop0();
138 128 throw e;
139 129 }
140 130 }
... ... @@ -144,6 +134,10 @@ public class IotHttpProtocol implements IotProtocol {
144 134 if (!running) {
145 135 return;
146 136 }
  137 + stop0();
  138 + }
  139 +
  140 + private void stop0() {
147 141 // 1. 停止下行消息订阅者
148 142 if (downstreamSubscriber != null) {
149 143 try {
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/handler/downstream/IotHttpDownstreamSubscriber.java
... ... @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.downstream;
3 3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5 5 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
6   -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
  6 +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber;
7 7 import lombok.extern.slf4j.Slf4j;
8 8  
9 9 /**
... ... @@ -13,7 +13,7 @@ import lombok.extern.slf4j.Slf4j;
13 13 */
14 14  
15 15 @Slf4j
16   -public class IotHttpDownstreamSubscriber extends IotProtocolDownstreamSubscriber {
  16 +public class IotHttpDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
17 17  
18 18 public IotHttpDownstreamSubscriber(IotProtocol protocol, IotMessageBus messageBus) {
19 19 super(protocol, messageBus);
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/manager/AbstractIotModbusPollScheduler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.manager;
  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 io.vertx.core.Vertx;
  7 +import lombok.AllArgsConstructor;
  8 +import lombok.Data;
  9 +import lombok.extern.slf4j.Slf4j;
  10 +
  11 +import java.util.*;
  12 +import java.util.concurrent.ConcurrentHashMap;
  13 +import java.util.concurrent.ConcurrentLinkedQueue;
  14 +
  15 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
  16 +
  17 +/**
  18 + * Modbus 轮询调度器基类
  19 + * <p>
  20 + * 封装通用的定时器管理、per-device 请求队列限速逻辑。
  21 + * 子类只需实现 {@link #pollPoint(Long, Long)} 定义具体的轮询动作。
  22 + * <p>
  23 + *
  24 + * @author 芋道源码
  25 + */
  26 +@Slf4j
  27 +public abstract class AbstractIotModbusPollScheduler {
  28 +
  29 + protected final Vertx vertx;
  30 +
  31 + /**
  32 + * 同设备最小请求间隔(毫秒),防止 Modbus 设备性能不足时请求堆积
  33 + */
  34 + private static final long MIN_REQUEST_INTERVAL = 1000;
  35 + /**
  36 + * 每个设备请求队列的最大长度,超出时丢弃最旧请求
  37 + */
  38 + private static final int MAX_QUEUE_SIZE = 1000;
  39 +
  40 + /**
  41 + * 设备点位的定时器映射:deviceId -> (pointId -> PointTimerInfo)
  42 + */
  43 + private final Map<Long, Map<Long, PointTimerInfo>> devicePointTimers = new ConcurrentHashMap<>();
  44 +
  45 + /**
  46 + * per-device 请求队列:deviceId -> 待执行请求队列
  47 + */
  48 + private final Map<Long, Queue<Runnable>> deviceRequestQueues = new ConcurrentHashMap<>();
  49 + /**
  50 + * per-device 上次请求时间戳:deviceId -> lastRequestTimeMs
  51 + */
  52 + private final Map<Long, Long> deviceLastRequestTime = new ConcurrentHashMap<>();
  53 + /**
  54 + * per-device 延迟 timer 标记:deviceId -> 是否有延迟 timer 在等待
  55 + */
  56 + private final Map<Long, Boolean> deviceDelayTimerActive = new ConcurrentHashMap<>();
  57 +
  58 + protected AbstractIotModbusPollScheduler(Vertx vertx) {
  59 + this.vertx = vertx;
  60 + }
  61 +
  62 + /**
  63 + * 点位定时器信息
  64 + */
  65 + @Data
  66 + @AllArgsConstructor
  67 + private static class PointTimerInfo {
  68 +
  69 + /**
  70 + * Vert.x 定时器 ID
  71 + */
  72 + private Long timerId;
  73 + /**
  74 + * 轮询间隔(用于判断是否需要更新定时器)
  75 + */
  76 + private Integer pollInterval;
  77 +
  78 + }
  79 +
  80 + // ========== 轮询管理 ==========
  81 +
  82 + /**
  83 + * 更新轮询任务(增量更新)
  84 + *
  85 + * 1. 【删除】点位:停止对应的轮询定时器
  86 + * 2. 【新增】点位:创建对应的轮询定时器
  87 + * 3. 【修改】点位:pollInterval 变化,重建对应的轮询定时器
  88 + * 【修改】其他属性变化:不需要重建定时器(pollPoint 运行时从 configCache 取最新 point)
  89 + */
  90 + public void updatePolling(IotModbusDeviceConfigRespDTO config) {
  91 + Long deviceId = config.getDeviceId();
  92 + List<IotModbusPointRespDTO> newPoints = config.getPoints();
  93 + Map<Long, PointTimerInfo> currentTimers = devicePointTimers
  94 + .computeIfAbsent(deviceId, k -> new ConcurrentHashMap<>());
  95 + // 1.1 计算新配置中的点位 ID 集合
  96 + Set<Long> newPointIds = convertSet(newPoints, IotModbusPointRespDTO::getId);
  97 + // 1.2 计算删除的点位 ID 集合
  98 + Set<Long> removedPointIds = new HashSet<>(currentTimers.keySet());
  99 + removedPointIds.removeAll(newPointIds);
  100 +
  101 + // 2. 处理删除的点位:停止不再存在的定时器
  102 + for (Long pointId : removedPointIds) {
  103 + PointTimerInfo timerInfo = currentTimers.remove(pointId);
  104 + if (timerInfo != null) {
  105 + vertx.cancelTimer(timerInfo.getTimerId());
  106 + log.debug("[updatePolling][设备 {} 点位 {} 定时器已删除]", deviceId, pointId);
  107 + }
  108 + }
  109 +
  110 + // 3. 处理新增和修改的点位
  111 + if (CollUtil.isEmpty(newPoints)) {
  112 + return;
  113 + }
  114 + for (IotModbusPointRespDTO point : newPoints) {
  115 + Long pointId = point.getId();
  116 + Integer newPollInterval = point.getPollInterval();
  117 + PointTimerInfo existingTimer = currentTimers.get(pointId);
  118 + // 3.1 新增点位:创建定时器
  119 + if (existingTimer == null) {
  120 + Long timerId = createPollTimer(deviceId, pointId, newPollInterval);
  121 + if (timerId != null) {
  122 + currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval));
  123 + log.debug("[updatePolling][设备 {} 点位 {} 定时器已创建, interval={}ms]",
  124 + deviceId, pointId, newPollInterval);
  125 + }
  126 + } else if (!Objects.equals(existingTimer.getPollInterval(), newPollInterval)) {
  127 + // 3.2 pollInterval 变化:重建定时器
  128 + vertx.cancelTimer(existingTimer.getTimerId());
  129 + Long timerId = createPollTimer(deviceId, pointId, newPollInterval);
  130 + if (timerId != null) {
  131 + currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval));
  132 + log.debug("[updatePolling][设备 {} 点位 {} 定时器已更新, interval={}ms -> {}ms]",
  133 + deviceId, pointId, existingTimer.getPollInterval(), newPollInterval);
  134 + } else {
  135 + currentTimers.remove(pointId);
  136 + }
  137 + }
  138 + // 3.3 其他属性变化:无需重建定时器,因为 pollPoint() 运行时从 configCache 获取最新 point,自动使用新配置
  139 + }
  140 + }
  141 +
  142 + /**
  143 + * 创建轮询定时器
  144 + */
  145 + private Long createPollTimer(Long deviceId, Long pointId, Integer pollInterval) {
  146 + if (pollInterval == null || pollInterval <= 0) {
  147 + return null;
  148 + }
  149 + return vertx.setPeriodic(pollInterval, timerId -> {
  150 + try {
  151 + submitPollRequest(deviceId, pointId);
  152 + } catch (Exception e) {
  153 + log.error("[createPollTimer][轮询点位失败, deviceId={}, pointId={}]", deviceId, pointId, e);
  154 + }
  155 + });
  156 + }
  157 +
  158 + // ========== 请求队列(per-device 限速) ==========
  159 +
  160 + /**
  161 + * 提交轮询请求到设备请求队列(保证同设备请求间隔)
  162 + */
  163 + private void submitPollRequest(Long deviceId, Long pointId) {
  164 + // 1. 【重要】将请求添加到设备的请求队列
  165 + Queue<Runnable> queue = deviceRequestQueues.computeIfAbsent(deviceId, k -> new ConcurrentLinkedQueue<>());
  166 + while (queue.size() >= MAX_QUEUE_SIZE) {
  167 + // 超出上限时,丢弃最旧的请求
  168 + queue.poll();
  169 + log.warn("[submitPollRequest][设备 {} 请求队列已满({}), 丢弃最旧请求]", deviceId, MAX_QUEUE_SIZE);
  170 + }
  171 + queue.offer(() -> pollPoint(deviceId, pointId));
  172 +
  173 + // 2. 处理设备请求队列(如果没有延迟 timer 在等待)
  174 + processDeviceQueue(deviceId);
  175 + }
  176 +
  177 + /**
  178 + * 处理设备请求队列
  179 + */
  180 + private void processDeviceQueue(Long deviceId) {
  181 + Queue<Runnable> queue = deviceRequestQueues.get(deviceId);
  182 + if (CollUtil.isEmpty(queue)) {
  183 + return;
  184 + }
  185 + // 检查是否已有延迟 timer 在等待
  186 + if (Boolean.TRUE.equals(deviceDelayTimerActive.get(deviceId))) {
  187 + return;
  188 + }
  189 +
  190 + // 不满足间隔要求,延迟执行
  191 + long now = System.currentTimeMillis();
  192 + long lastTime = deviceLastRequestTime.getOrDefault(deviceId, 0L);
  193 + long elapsed = now - lastTime;
  194 + if (elapsed < MIN_REQUEST_INTERVAL) {
  195 + scheduleNextRequest(deviceId, MIN_REQUEST_INTERVAL - elapsed);
  196 + return;
  197 + }
  198 +
  199 + // 满足间隔要求,立即执行
  200 + Runnable task = queue.poll();
  201 + if (task == null) {
  202 + return;
  203 + }
  204 + deviceLastRequestTime.put(deviceId, now);
  205 + task.run();
  206 + // 继续处理队列中的下一个(如果有的话,需要延迟)
  207 + if (CollUtil.isNotEmpty(queue)) {
  208 + scheduleNextRequest(deviceId);
  209 + }
  210 + }
  211 +
  212 + private void scheduleNextRequest(Long deviceId) {
  213 + scheduleNextRequest(deviceId, MIN_REQUEST_INTERVAL);
  214 + }
  215 +
  216 + private void scheduleNextRequest(Long deviceId, long delayMs) {
  217 + deviceDelayTimerActive.put(deviceId, true);
  218 + vertx.setTimer(delayMs, id -> {
  219 + deviceDelayTimerActive.put(deviceId, false);
  220 + Queue<Runnable> queue = deviceRequestQueues.get(deviceId);
  221 + if (CollUtil.isEmpty(queue)) {
  222 + return;
  223 + }
  224 +
  225 + // 满足间隔要求,立即执行
  226 + Runnable task = queue.poll();
  227 + if (task == null) {
  228 + return;
  229 + }
  230 + deviceLastRequestTime.put(deviceId, System.currentTimeMillis());
  231 + task.run();
  232 + // 继续处理队列中的下一个(如果有的话,需要延迟)
  233 + if (CollUtil.isNotEmpty(queue)) {
  234 + scheduleNextRequest(deviceId);
  235 + }
  236 + });
  237 + }
  238 +
  239 + // ========== 轮询执行 ==========
  240 +
  241 + /**
  242 + * 轮询单个点位(子类实现具体的读取逻辑)
  243 + *
  244 + * @param deviceId 设备 ID
  245 + * @param pointId 点位 ID
  246 + */
  247 + protected abstract void pollPoint(Long deviceId, Long pointId);
  248 +
  249 + // ========== 停止 ==========
  250 +
  251 + /**
  252 + * 停止设备的轮询
  253 + */
  254 + public void stopPolling(Long deviceId) {
  255 + Map<Long, PointTimerInfo> timers = devicePointTimers.remove(deviceId);
  256 + if (CollUtil.isEmpty(timers)) {
  257 + return;
  258 + }
  259 + for (PointTimerInfo timerInfo : timers.values()) {
  260 + vertx.cancelTimer(timerInfo.getTimerId());
  261 + }
  262 + // 清理请求队列
  263 + deviceRequestQueues.remove(deviceId);
  264 + deviceLastRequestTime.remove(deviceId);
  265 + deviceDelayTimerActive.remove(deviceId);
  266 + log.debug("[stopPolling][设备 {} 停止了 {} 个轮询定时器]", deviceId, timers.size());
  267 + }
  268 +
  269 + /**
  270 + * 停止所有轮询
  271 + */
  272 + public void stopAll() {
  273 + for (Long deviceId : new ArrayList<>(devicePointTimers.keySet())) {
  274 + stopPolling(deviceId);
  275 + }
  276 + }
  277 +
  278 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusCommonUtils.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.ArrayUtil;
  5 +import cn.hutool.core.util.ObjectUtil;
  6 +import cn.hutool.core.util.StrUtil;
  7 +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
  8 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
  9 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
  10 +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusByteOrderEnum;
  11 +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusRawDataTypeEnum;
  12 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrame;
  13 +import lombok.experimental.UtilityClass;
  14 +import lombok.extern.slf4j.Slf4j;
  15 +
  16 +import java.math.BigDecimal;
  17 +import java.math.RoundingMode;
  18 +import java.nio.ByteBuffer;
  19 +import java.nio.ByteOrder;
  20 +
  21 +/**
  22 + * IoT Modbus 协议工具类
  23 + * <p>
  24 + * 提供 Modbus 协议全链路能力:
  25 + * <ul>
  26 + * <li>协议常量:功能码(FC01~FC16)、异常掩码等</li>
  27 + * <li>功能码判断:读/写/异常分类、可写判断、写功能码映射</li>
  28 + * <li>CRC-16/MODBUS 计算和校验</li>
  29 + * <li>数据转换:原始值 ↔ 物模型属性值({@link #convertToPropertyValue} / {@link #convertToRawValues})</li>
  30 + * <li>帧值提取:从 Modbus 帧提取寄存器/线圈值({@link #extractValues})</li>
  31 + * <li>点位查找({@link #findPoint})</li>
  32 + * </ul>
  33 + *
  34 + * @author 芋道源码
  35 + */
  36 +@UtilityClass
  37 +@Slf4j
  38 +public class IotModbusCommonUtils {
  39 +
  40 + /** FC01: 读线圈 */
  41 + public static final int FC_READ_COILS = 1;
  42 + /** FC02: 读离散输入 */
  43 + public static final int FC_READ_DISCRETE_INPUTS = 2;
  44 + /** FC03: 读保持寄存器 */
  45 + public static final int FC_READ_HOLDING_REGISTERS = 3;
  46 + /** FC04: 读输入寄存器 */
  47 + public static final int FC_READ_INPUT_REGISTERS = 4;
  48 +
  49 + /** FC05: 写单个线圈 */
  50 + public static final int FC_WRITE_SINGLE_COIL = 5;
  51 + /** FC06: 写单个寄存器 */
  52 + public static final int FC_WRITE_SINGLE_REGISTER = 6;
  53 + /** FC15: 写多个线圈 */
  54 + public static final int FC_WRITE_MULTIPLE_COILS = 15;
  55 + /** FC16: 写多个寄存器 */
  56 + public static final int FC_WRITE_MULTIPLE_REGISTERS = 16;
  57 +
  58 + /**
  59 + * 异常响应掩码:响应帧的功能码最高位为 1 时,表示异常响应
  60 + * 例如:请求 FC=0x03,异常响应 FC=0x83(0x03 | 0x80)
  61 + */
  62 + public static final int FC_EXCEPTION_MASK = 0x80;
  63 +
  64 + /**
  65 + * 功能码掩码:用于从异常响应中提取原始功能码
  66 + * 例如:异常 FC=0x83,原始 FC = 0x83 & 0x7F = 0x03
  67 + */
  68 + public static final int FC_MASK = 0x7F;
  69 +
  70 + // ==================== 功能码分类判断 ====================
  71 +
  72 + /**
  73 + * 判断是否为读响应(FC01-04)
  74 + */
  75 + public static boolean isReadResponse(int functionCode) {
  76 + return functionCode >= FC_READ_COILS && functionCode <= FC_READ_INPUT_REGISTERS;
  77 + }
  78 +
  79 + /**
  80 + * 判断是否为写响应(FC05/06/15/16)
  81 + */
  82 + public static boolean isWriteResponse(int functionCode) {
  83 + return functionCode == FC_WRITE_SINGLE_COIL || functionCode == FC_WRITE_SINGLE_REGISTER
  84 + || functionCode == FC_WRITE_MULTIPLE_COILS || functionCode == FC_WRITE_MULTIPLE_REGISTERS;
  85 + }
  86 +
  87 + /**
  88 + * 判断是否为异常响应
  89 + */
  90 + public static boolean isExceptionResponse(int functionCode) {
  91 + return (functionCode & FC_EXCEPTION_MASK) != 0;
  92 + }
  93 +
  94 + /**
  95 + * 从异常响应中提取原始功能码
  96 + */
  97 + public static int extractOriginalFunctionCode(int exceptionFunctionCode) {
  98 + return exceptionFunctionCode & FC_MASK;
  99 + }
  100 +
  101 + /**
  102 + * 判断读功能码是否支持写操作
  103 + * <p>
  104 + * FC01(读线圈)和 FC03(读保持寄存器)支持写操作;
  105 + * FC02(读离散输入)和 FC04(读输入寄存器)为只读。
  106 + *
  107 + * @param readFunctionCode 读功能码(FC01-04)
  108 + * @return 是否支持写操作
  109 + */
  110 + @SuppressWarnings("BooleanMethodIsAlwaysInverted")
  111 + public static boolean isWritable(int readFunctionCode) {
  112 + return readFunctionCode == FC_READ_COILS || readFunctionCode == FC_READ_HOLDING_REGISTERS;
  113 + }
  114 +
  115 + /**
  116 + * 获取单写功能码
  117 + * <p>
  118 + * FC01(读线圈)→ FC05(写单个线圈);
  119 + * FC03(读保持寄存器)→ FC06(写单个寄存器);
  120 + * 其他返回 null(不支持写)。
  121 + *
  122 + * @param readFunctionCode 读功能码
  123 + * @return 单写功能码,不支持写时返回 null
  124 + */
  125 + @SuppressWarnings("EnhancedSwitchMigration")
  126 + public static Integer getWriteSingleFunctionCode(int readFunctionCode) {
  127 + switch (readFunctionCode) {
  128 + case FC_READ_COILS:
  129 + return FC_WRITE_SINGLE_COIL;
  130 + case FC_READ_HOLDING_REGISTERS:
  131 + return FC_WRITE_SINGLE_REGISTER;
  132 + default:
  133 + return null;
  134 + }
  135 + }
  136 +
  137 + /**
  138 + * 获取多写功能码
  139 + * <p>
  140 + * FC01(读线圈)→ FC15(写多个线圈);
  141 + * FC03(读保持寄存器)→ FC16(写多个寄存器);
  142 + * 其他返回 null(不支持写)。
  143 + *
  144 + * @param readFunctionCode 读功能码
  145 + * @return 多写功能码,不支持写时返回 null
  146 + */
  147 + @SuppressWarnings("EnhancedSwitchMigration")
  148 + public static Integer getWriteMultipleFunctionCode(int readFunctionCode) {
  149 + switch (readFunctionCode) {
  150 + case FC_READ_COILS:
  151 + return FC_WRITE_MULTIPLE_COILS;
  152 + case FC_READ_HOLDING_REGISTERS:
  153 + return FC_WRITE_MULTIPLE_REGISTERS;
  154 + default:
  155 + return null;
  156 + }
  157 + }
  158 +
  159 + // ==================== CRC16 工具 ====================
  160 +
  161 + /**
  162 + * 计算 CRC-16/MODBUS
  163 + *
  164 + * @param data 数据
  165 + * @param length 计算长度
  166 + * @return CRC16 值
  167 + */
  168 + public static int calculateCrc16(byte[] data, int length) {
  169 + int crc = 0xFFFF;
  170 + for (int i = 0; i < length; i++) {
  171 + crc ^= (data[i] & 0xFF);
  172 + for (int j = 0; j < 8; j++) {
  173 + if ((crc & 0x0001) != 0) {
  174 + crc >>= 1;
  175 + crc ^= 0xA001;
  176 + } else {
  177 + crc >>= 1;
  178 + }
  179 + }
  180 + }
  181 + return crc;
  182 + }
  183 +
  184 + /**
  185 + * 校验 CRC16
  186 + *
  187 + * @param data 包含 CRC 的完整数据
  188 + * @return 校验是否通过
  189 + */
  190 + public static boolean verifyCrc16(byte[] data) {
  191 + if (data.length < 3) {
  192 + return false;
  193 + }
  194 + int computed = calculateCrc16(data, data.length - 2);
  195 + int received = (data[data.length - 2] & 0xFF) | ((data[data.length - 1] & 0xFF) << 8);
  196 + return computed == received;
  197 + }
  198 +
  199 + // ==================== 数据转换 ====================
  200 +
  201 + /**
  202 + * 将原始值转换为物模型属性值
  203 + *
  204 + * @param rawValues 原始值数组(寄存器值或线圈值)
  205 + * @param point 点位配置
  206 + * @return 转换后的属性值
  207 + */
  208 + public static Object convertToPropertyValue(int[] rawValues, IotModbusPointRespDTO point) {
  209 + if (ArrayUtil.isEmpty(rawValues)) {
  210 + return null;
  211 + }
  212 + String rawDataType = point.getRawDataType();
  213 + String byteOrder = point.getByteOrder();
  214 + BigDecimal scale = ObjectUtil.defaultIfNull(point.getScale(), BigDecimal.ONE);
  215 +
  216 + // 1. 根据原始数据类型解析原始数值
  217 + Number rawNumber = parseRawValue(rawValues, rawDataType, byteOrder);
  218 + if (rawNumber == null) {
  219 + return null;
  220 + }
  221 +
  222 + // 2. 应用缩放因子:实际值 = 原始值 × scale
  223 + BigDecimal actualValue = new BigDecimal(rawNumber.toString()).multiply(scale);
  224 +
  225 + // 3. 根据数据类型返回合适的 Java 类型
  226 + return formatValue(actualValue, rawDataType);
  227 + }
  228 +
  229 + /**
  230 + * 将物模型属性值转换为原始寄存器值
  231 + *
  232 + * @param propertyValue 属性值
  233 + * @param point 点位配置
  234 + * @return 原始值数组
  235 + */
  236 + public static int[] convertToRawValues(Object propertyValue, IotModbusPointRespDTO point) {
  237 + if (propertyValue == null) {
  238 + return new int[0];
  239 + }
  240 + String rawDataType = point.getRawDataType();
  241 + String byteOrder = point.getByteOrder();
  242 + BigDecimal scale = ObjectUtil.defaultIfNull(point.getScale(), BigDecimal.ONE);
  243 + int registerCount = ObjectUtil.defaultIfNull(point.getRegisterCount(), 1);
  244 +
  245 + // 1. 转换为 BigDecimal
  246 + BigDecimal actualValue = new BigDecimal(propertyValue.toString());
  247 +
  248 + // 2. 应用缩放因子:原始值 = 实际值 ÷ scale
  249 + BigDecimal rawValue = actualValue.divide(scale, 0, RoundingMode.HALF_UP);
  250 +
  251 + // 3. 根据原始数据类型编码为寄存器值
  252 + return encodeToRegisters(rawValue, rawDataType, byteOrder, registerCount);
  253 + }
  254 +
  255 + @SuppressWarnings("EnhancedSwitchMigration")
  256 + private static Number parseRawValue(int[] rawValues, String rawDataType, String byteOrder) {
  257 + IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType);
  258 + if (dataTypeEnum == null) {
  259 + log.warn("[parseRawValue][不支持的数据类型: {}]", rawDataType);
  260 + return rawValues[0];
  261 + }
  262 + switch (dataTypeEnum) {
  263 + case BOOLEAN:
  264 + return rawValues[0] != 0 ? 1 : 0;
  265 + case INT16:
  266 + return (short) rawValues[0];
  267 + case UINT16:
  268 + return rawValues[0] & 0xFFFF;
  269 + case INT32:
  270 + return parseInt32(rawValues, byteOrder);
  271 + case UINT32:
  272 + return parseUint32(rawValues, byteOrder);
  273 + case FLOAT:
  274 + return parseFloat(rawValues, byteOrder);
  275 + case DOUBLE:
  276 + return parseDouble(rawValues, byteOrder);
  277 + default:
  278 + log.warn("[parseRawValue][不支持的数据类型: {}]", rawDataType);
  279 + return rawValues[0];
  280 + }
  281 + }
  282 +
  283 + private static int parseInt32(int[] rawValues, String byteOrder) {
  284 + if (rawValues.length < 2) {
  285 + return rawValues[0];
  286 + }
  287 + byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder);
  288 + return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getInt();
  289 + }
  290 +
  291 + private static long parseUint32(int[] rawValues, String byteOrder) {
  292 + if (rawValues.length < 2) {
  293 + return rawValues[0] & 0xFFFFFFFFL;
  294 + }
  295 + byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder);
  296 + return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getInt() & 0xFFFFFFFFL;
  297 + }
  298 +
  299 + private static float parseFloat(int[] rawValues, String byteOrder) {
  300 + if (rawValues.length < 2) {
  301 + return (float) rawValues[0];
  302 + }
  303 + byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder);
  304 + return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getFloat();
  305 + }
  306 +
  307 + private static double parseDouble(int[] rawValues, String byteOrder) {
  308 + if (rawValues.length < 4) {
  309 + return rawValues[0];
  310 + }
  311 + byte[] bytes = reorderBytes(registersToBytes(rawValues, 4), byteOrder);
  312 + return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getDouble();
  313 + }
  314 +
  315 + private static byte[] registersToBytes(int[] registers, int count) {
  316 + byte[] bytes = new byte[count * 2];
  317 + for (int i = 0; i < Math.min(registers.length, count); i++) {
  318 + bytes[i * 2] = (byte) ((registers[i] >> 8) & 0xFF);
  319 + bytes[i * 2 + 1] = (byte) (registers[i] & 0xFF);
  320 + }
  321 + return bytes;
  322 + }
  323 +
  324 + @SuppressWarnings("EnhancedSwitchMigration")
  325 + private static byte[] reorderBytes(byte[] bytes, String byteOrder) {
  326 + IotModbusByteOrderEnum byteOrderEnum = IotModbusByteOrderEnum.getByOrder(byteOrder);
  327 + // null 或者大端序,不需要调整
  328 + if (ObjectUtils.equalsAny(byteOrderEnum, null, IotModbusByteOrderEnum.ABCD, IotModbusByteOrderEnum.AB)) {
  329 + return bytes;
  330 + }
  331 +
  332 + // 其他字节序调整
  333 + byte[] result = new byte[bytes.length];
  334 + switch (byteOrderEnum) {
  335 + case BA: // 小端序:按每 2 字节一组交换(16 位场景 [1,0],32 位场景 [1,0,3,2])
  336 + for (int i = 0; i + 1 < bytes.length; i += 2) {
  337 + result[i] = bytes[i + 1];
  338 + result[i + 1] = bytes[i];
  339 + }
  340 + break;
  341 + case CDAB: // 大端字交换(32 位)
  342 + if (bytes.length >= 4) {
  343 + result[0] = bytes[2];
  344 + result[1] = bytes[3];
  345 + result[2] = bytes[0];
  346 + result[3] = bytes[1];
  347 + }
  348 + break;
  349 + case DCBA: // 小端序(32 位)
  350 + if (bytes.length >= 4) {
  351 + result[0] = bytes[3];
  352 + result[1] = bytes[2];
  353 + result[2] = bytes[1];
  354 + result[3] = bytes[0];
  355 + }
  356 + break;
  357 + case BADC: // 小端字交换(32 位)
  358 + if (bytes.length >= 4) {
  359 + result[0] = bytes[1];
  360 + result[1] = bytes[0];
  361 + result[2] = bytes[3];
  362 + result[3] = bytes[2];
  363 + }
  364 + break;
  365 + default:
  366 + return bytes;
  367 + }
  368 + return result;
  369 + }
  370 +
  371 + @SuppressWarnings("EnhancedSwitchMigration")
  372 + private static int[] encodeToRegisters(BigDecimal rawValue, String rawDataType, String byteOrder, int registerCount) {
  373 + IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType);
  374 + if (dataTypeEnum == null) {
  375 + return new int[]{rawValue.intValue()};
  376 + }
  377 + switch (dataTypeEnum) {
  378 + case BOOLEAN:
  379 + return new int[]{rawValue.intValue() != 0 ? 1 : 0};
  380 + case INT16:
  381 + case UINT16:
  382 + return new int[]{rawValue.intValue() & 0xFFFF};
  383 + case INT32:
  384 + return encodeInt32(rawValue.intValue(), byteOrder);
  385 + case UINT32:
  386 + // 使用 longValue() 避免超过 Integer.MAX_VALUE 时溢出,
  387 + // 强转 int 保留低 32 位 bit pattern,写入寄存器的字节是正确的无符号值
  388 + return encodeInt32((int) rawValue.longValue(), byteOrder);
  389 + case FLOAT:
  390 + return encodeFloat(rawValue.floatValue(), byteOrder);
  391 + case DOUBLE:
  392 + return encodeDouble(rawValue.doubleValue(), byteOrder);
  393 + default:
  394 + return new int[]{rawValue.intValue()};
  395 + }
  396 + }
  397 +
  398 + private static int[] encodeInt32(int value, String byteOrder) {
  399 + byte[] bytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(value).array();
  400 + bytes = reorderBytes(bytes, byteOrder);
  401 + return bytesToRegisters(bytes);
  402 + }
  403 +
  404 + private static int[] encodeFloat(float value, String byteOrder) {
  405 + byte[] bytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putFloat(value).array();
  406 + bytes = reorderBytes(bytes, byteOrder);
  407 + return bytesToRegisters(bytes);
  408 + }
  409 +
  410 + private static int[] encodeDouble(double value, String byteOrder) {
  411 + byte[] bytes = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN).putDouble(value).array();
  412 + bytes = reorderBytes(bytes, byteOrder);
  413 + return bytesToRegisters(bytes);
  414 + }
  415 +
  416 + private static int[] bytesToRegisters(byte[] bytes) {
  417 + int[] registers = new int[bytes.length / 2];
  418 + for (int i = 0; i < registers.length; i++) {
  419 + registers[i] = ((bytes[i * 2] & 0xFF) << 8) | (bytes[i * 2 + 1] & 0xFF);
  420 + }
  421 + return registers;
  422 + }
  423 +
  424 + @SuppressWarnings("EnhancedSwitchMigration")
  425 + private static Object formatValue(BigDecimal value, String rawDataType) {
  426 + IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType);
  427 + if (dataTypeEnum == null) {
  428 + return value;
  429 + }
  430 + switch (dataTypeEnum) {
  431 + case BOOLEAN:
  432 + return value.intValue() != 0;
  433 + case INT16:
  434 + case INT32:
  435 + return value.intValue();
  436 + case UINT16:
  437 + case UINT32:
  438 + return value.longValue();
  439 + case FLOAT:
  440 + return value.floatValue();
  441 + case DOUBLE:
  442 + return value.doubleValue();
  443 + default:
  444 + return value;
  445 + }
  446 + }
  447 +
  448 + // ==================== 帧值提取 ====================
  449 +
  450 + /**
  451 + * 从帧中提取寄存器值(FC01-04 读响应)
  452 + *
  453 + * @param frame 解码后的 Modbus 帧
  454 + * @return 寄存器值数组(int[]),失败返回 null
  455 + */
  456 + @SuppressWarnings("EnhancedSwitchMigration")
  457 + public static int[] extractValues(IotModbusFrame frame) {
  458 + if (frame == null || frame.isException()) {
  459 + return null;
  460 + }
  461 + byte[] pdu = frame.getPdu();
  462 + if (pdu == null || pdu.length < 1) {
  463 + return null;
  464 + }
  465 +
  466 + int functionCode = frame.getFunctionCode();
  467 + switch (functionCode) {
  468 + case FC_READ_COILS:
  469 + case FC_READ_DISCRETE_INPUTS:
  470 + return extractCoilValues(pdu);
  471 + case FC_READ_HOLDING_REGISTERS:
  472 + case FC_READ_INPUT_REGISTERS:
  473 + return extractRegisterValues(pdu);
  474 + default:
  475 + log.warn("[extractValues][不支持的功能码: {}]", functionCode);
  476 + return null;
  477 + }
  478 + }
  479 +
  480 + private static int[] extractCoilValues(byte[] pdu) {
  481 + if (pdu.length < 2) {
  482 + return null;
  483 + }
  484 + int byteCount = pdu[0] & 0xFF;
  485 + int bitCount = byteCount * 8;
  486 + int[] values = new int[bitCount];
  487 + for (int i = 0; i < bitCount && (1 + i / 8) < pdu.length; i++) {
  488 + values[i] = ((pdu[1 + i / 8] >> (i % 8)) & 0x01);
  489 + }
  490 + return values;
  491 + }
  492 +
  493 + private static int[] extractRegisterValues(byte[] pdu) {
  494 + if (pdu.length < 2) {
  495 + return null;
  496 + }
  497 + int byteCount = pdu[0] & 0xFF;
  498 + int registerCount = byteCount / 2;
  499 + int[] values = new int[registerCount];
  500 + for (int i = 0; i < registerCount && (1 + i * 2 + 1) < pdu.length; i++) {
  501 + values[i] = ((pdu[1 + i * 2] & 0xFF) << 8) | (pdu[1 + i * 2 + 1] & 0xFF);
  502 + }
  503 + return values;
  504 + }
  505 +
  506 + /**
  507 + * 从响应帧中提取 registerCount(通过 PDU 的 byteCount 推断)
  508 + *
  509 + * @param frame 解码后的 Modbus 响应帧
  510 + * @return registerCount,无法提取时返回 -1(匹配时跳过校验)
  511 + */
  512 + public static int extractRegisterCountFromResponse(IotModbusFrame frame) {
  513 + byte[] pdu = frame.getPdu();
  514 + if (pdu == null || pdu.length < 1) {
  515 + return -1;
  516 + }
  517 + int byteCount = pdu[0] & 0xFF;
  518 + int fc = frame.getFunctionCode();
  519 + // FC03/04 寄存器读响应:registerCount = byteCount / 2
  520 + if (fc == FC_READ_HOLDING_REGISTERS || fc == FC_READ_INPUT_REGISTERS) {
  521 + return byteCount / 2;
  522 + }
  523 + // FC01/02 线圈/离散输入读响应:按 bit 打包有余位,无法精确反推,返回 -1 跳过校验
  524 + return -1;
  525 + }
  526 +
  527 + // ==================== 点位查找 ====================
  528 +
  529 + /**
  530 + * 查找点位配置
  531 + *
  532 + * @param config 设备 Modbus 配置
  533 + * @param identifier 点位标识符
  534 + * @return 匹配的点位配置,未找到返回 null
  535 + */
  536 + public static IotModbusPointRespDTO findPoint(IotModbusDeviceConfigRespDTO config, String identifier) {
  537 + if (config == null || StrUtil.isBlank(identifier)) {
  538 + return null;
  539 + }
  540 + return CollUtil.findOne(config.getPoints(), p -> identifier.equals(p.getIdentifier()));
  541 + }
  542 +
  543 + /**
  544 + * 根据点位 ID 查找点位配置
  545 + *
  546 + * @param config 设备 Modbus 配置
  547 + * @param pointId 点位 ID
  548 + * @return 匹配的点位配置,未找到返回 null
  549 + */
  550 + public static IotModbusPointRespDTO findPointById(IotModbusDeviceConfigRespDTO config, Long pointId) {
  551 + if (config == null || pointId == null) {
  552 + return null;
  553 + }
  554 + return CollUtil.findOne(config.getPoints(), p -> p.getId().equals(pointId));
  555 + }
  556 +
  557 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/common/utils/IotModbusTcpClientUtils.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
  4 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConnectionManager;
  5 +import com.ghgande.j2mod.modbus.io.ModbusTCPTransaction;
  6 +import com.ghgande.j2mod.modbus.msg.*;
  7 +import com.ghgande.j2mod.modbus.procimg.InputRegister;
  8 +import com.ghgande.j2mod.modbus.procimg.Register;
  9 +import com.ghgande.j2mod.modbus.procimg.SimpleRegister;
  10 +import com.ghgande.j2mod.modbus.util.BitVector;
  11 +import io.vertx.core.Future;
  12 +import lombok.experimental.UtilityClass;
  13 +import lombok.extern.slf4j.Slf4j;
  14 +
  15 +import static cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils.*;
  16 +
  17 +/**
  18 + * IoT Modbus TCP 客户端工具类
  19 + * <p>
  20 + * 封装基于 j2mod 的 Modbus TCP 读写操作:
  21 + * 1. 根据功能码创建对应的 Modbus 读/写请求
  22 + * 2. 通过 {@link IotModbusTcpClientConnectionManager.ModbusConnection} 执行事务
  23 + * 3. 从响应中提取原始值
  24 + *
  25 + * @author 芋道源码
  26 + */
  27 +@UtilityClass
  28 +@Slf4j
  29 +public class IotModbusTcpClientUtils {
  30 +
  31 + /**
  32 + * 读取 Modbus 数据
  33 + *
  34 + * @param connection Modbus 连接
  35 + * @param slaveId 从站地址
  36 + * @param point 点位配置
  37 + * @return 原始值(int 数组)
  38 + */
  39 + public static Future<int[]> read(IotModbusTcpClientConnectionManager.ModbusConnection connection,
  40 + Integer slaveId,
  41 + IotModbusPointRespDTO point) {
  42 + return connection.executeBlocking(tcpConnection -> {
  43 + try {
  44 + // 1. 创建请求
  45 + ModbusRequest request = createReadRequest(point.getFunctionCode(),
  46 + point.getRegisterAddress(), point.getRegisterCount());
  47 + request.setUnitID(slaveId);
  48 +
  49 + // 2. 执行事务(请求)
  50 + ModbusTCPTransaction transaction = new ModbusTCPTransaction(tcpConnection);
  51 + transaction.setRequest(request);
  52 + transaction.execute();
  53 +
  54 + // 3. 解析响应
  55 + ModbusResponse response = transaction.getResponse();
  56 + return extractValues(response, point.getFunctionCode());
  57 + } catch (Exception e) {
  58 + throw new RuntimeException(String.format("Modbus 读取失败 [slaveId=%d, identifier=%s, address=%d]",
  59 + slaveId, point.getIdentifier(), point.getRegisterAddress()), e);
  60 + }
  61 + });
  62 + }
  63 +
  64 + /**
  65 + * 写入 Modbus 数据
  66 + *
  67 + * @param connection Modbus 连接
  68 + * @param slaveId 从站地址
  69 + * @param point 点位配置
  70 + * @param values 要写入的值
  71 + * @return 是否成功
  72 + */
  73 + public static Future<Boolean> write(IotModbusTcpClientConnectionManager.ModbusConnection connection,
  74 + Integer slaveId,
  75 + IotModbusPointRespDTO point,
  76 + int[] values) {
  77 + return connection.executeBlocking(tcpConnection -> {
  78 + try {
  79 + // 1. 创建请求
  80 + ModbusRequest request = createWriteRequest(point.getFunctionCode(),
  81 + point.getRegisterAddress(), point.getRegisterCount(), values);
  82 + if (request == null) {
  83 + throw new RuntimeException("功能码 " + point.getFunctionCode() + " 不支持写操作");
  84 + }
  85 + request.setUnitID(slaveId);
  86 +
  87 + // 2. 执行事务(请求)
  88 + ModbusTCPTransaction transaction = new ModbusTCPTransaction(tcpConnection);
  89 + transaction.setRequest(request);
  90 + transaction.execute();
  91 + return true;
  92 + } catch (Exception e) {
  93 + throw new RuntimeException(String.format("Modbus 写入失败 [slaveId=%d, identifier=%s, address=%d]",
  94 + slaveId, point.getIdentifier(), point.getRegisterAddress()), e);
  95 + }
  96 + });
  97 + }
  98 +
  99 + /**
  100 + * 创建读取请求
  101 + */
  102 + @SuppressWarnings("EnhancedSwitchMigration")
  103 + private static ModbusRequest createReadRequest(Integer functionCode, Integer address, Integer count) {
  104 + switch (functionCode) {
  105 + case FC_READ_COILS:
  106 + return new ReadCoilsRequest(address, count);
  107 + case FC_READ_DISCRETE_INPUTS:
  108 + return new ReadInputDiscretesRequest(address, count);
  109 + case FC_READ_HOLDING_REGISTERS:
  110 + return new ReadMultipleRegistersRequest(address, count);
  111 + case FC_READ_INPUT_REGISTERS:
  112 + return new ReadInputRegistersRequest(address, count);
  113 + default:
  114 + throw new IllegalArgumentException("不支持的功能码: " + functionCode);
  115 + }
  116 + }
  117 +
  118 + /**
  119 + * 创建写入请求
  120 + */
  121 + @SuppressWarnings("EnhancedSwitchMigration")
  122 + private static ModbusRequest createWriteRequest(Integer functionCode, Integer address, Integer count, int[] values) {
  123 + switch (functionCode) {
  124 + case FC_READ_COILS: // 写线圈(使用功能码 5 或 15)
  125 + if (count == 1) {
  126 + return new WriteCoilRequest(address, values[0] != 0);
  127 + } else {
  128 + BitVector bv = new BitVector(count);
  129 + for (int i = 0; i < Math.min(values.length, count); i++) {
  130 + bv.setBit(i, values[i] != 0);
  131 + }
  132 + return new WriteMultipleCoilsRequest(address, bv);
  133 + }
  134 + case FC_READ_HOLDING_REGISTERS: // 写保持寄存器(使用功能码 6 或 16)
  135 + if (count == 1) {
  136 + return new WriteSingleRegisterRequest(address, new SimpleRegister(values[0]));
  137 + } else {
  138 + Register[] registers = new SimpleRegister[count];
  139 + for (int i = 0; i < count; i++) {
  140 + registers[i] = new SimpleRegister(i < values.length ? values[i] : 0);
  141 + }
  142 + return new WriteMultipleRegistersRequest(address, registers);
  143 + }
  144 + case FC_READ_DISCRETE_INPUTS: // 只读
  145 + case FC_READ_INPUT_REGISTERS: // 只读
  146 + return null;
  147 + default:
  148 + throw new IllegalArgumentException("不支持的功能码: " + functionCode);
  149 + }
  150 + }
  151 +
  152 + /**
  153 + * 从响应中提取值
  154 + */
  155 + @SuppressWarnings("EnhancedSwitchMigration")
  156 + private static int[] extractValues(ModbusResponse response, Integer functionCode) {
  157 + switch (functionCode) {
  158 + case FC_READ_COILS:
  159 + ReadCoilsResponse coilsResponse = (ReadCoilsResponse) response;
  160 + int bitCount = coilsResponse.getBitCount();
  161 + int[] coilValues = new int[bitCount];
  162 + for (int i = 0; i < bitCount; i++) {
  163 + coilValues[i] = coilsResponse.getCoilStatus(i) ? 1 : 0;
  164 + }
  165 + return coilValues;
  166 + case FC_READ_DISCRETE_INPUTS:
  167 + ReadInputDiscretesResponse discretesResponse = (ReadInputDiscretesResponse) response;
  168 + int discreteCount = discretesResponse.getBitCount();
  169 + int[] discreteValues = new int[discreteCount];
  170 + for (int i = 0; i < discreteCount; i++) {
  171 + discreteValues[i] = discretesResponse.getDiscreteStatus(i) ? 1 : 0;
  172 + }
  173 + return discreteValues;
  174 + case FC_READ_HOLDING_REGISTERS:
  175 + ReadMultipleRegistersResponse holdingResponse = (ReadMultipleRegistersResponse) response;
  176 + InputRegister[] holdingRegisters = holdingResponse.getRegisters();
  177 + int[] holdingValues = new int[holdingRegisters.length];
  178 + for (int i = 0; i < holdingRegisters.length; i++) {
  179 + holdingValues[i] = holdingRegisters[i].getValue();
  180 + }
  181 + return holdingValues;
  182 + case FC_READ_INPUT_REGISTERS:
  183 + ReadInputRegistersResponse inputResponse = (ReadInputRegistersResponse) response;
  184 + InputRegister[] inputRegisters = inputResponse.getRegisters();
  185 + int[] inputValues = new int[inputRegisters.length];
  186 + for (int i = 0; i < inputRegisters.length; i++) {
  187 + inputValues[i] = inputRegisters[i].getValue();
  188 + }
  189 + return inputValues;
  190 + default:
  191 + throw new IllegalArgumentException("不支持的功能码: " + functionCode);
  192 + }
  193 + }
  194 +
  195 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/IotModbusTcpClientConfig.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient;
  2 +
  3 +import jakarta.validation.constraints.Min;
  4 +import jakarta.validation.constraints.NotNull;
  5 +import lombok.Data;
  6 +
  7 +/**
  8 + * IoT Modbus TCP Client 协议配置
  9 + *
  10 + * @author 芋道源码
  11 + */
  12 +@Data
  13 +public class IotModbusTcpClientConfig {
  14 +
  15 + /**
  16 + * 配置刷新间隔(秒)
  17 + */
  18 + @NotNull(message = "配置刷新间隔不能为空")
  19 + @Min(value = 1, message = "配置刷新间隔不能小于 1 秒")
  20 + private Integer configRefreshInterval = 30;
  21 +
  22 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/IotModbusTcpClientProtocol.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient;
  2 +
  3 +import cn.hutool.core.lang.Assert;
  4 +import cn.hutool.extra.spring.SpringUtil;
  5 +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
  6 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
  7 +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
  8 +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
  9 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
  10 +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
  11 +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
  12 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.downstream.IotModbusTcpClientDownstreamHandler;
  13 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.downstream.IotModbusTcpClientDownstreamSubscriber;
  14 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.upstream.IotModbusTcpClientUpstreamHandler;
  15 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConfigCacheService;
  16 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConnectionManager;
  17 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientPollScheduler;
  18 +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  19 +import io.vertx.core.Vertx;
  20 +import lombok.Getter;
  21 +import lombok.extern.slf4j.Slf4j;
  22 +import org.redisson.api.RedissonClient;
  23 +
  24 +import java.util.List;
  25 +import java.util.Set;
  26 +import java.util.concurrent.TimeUnit;
  27 +
  28 +/**
  29 + * IoT 网关 Modbus TCP Client 协议:主动轮询 Modbus 从站设备数据
  30 + *
  31 + * @author 芋道源码
  32 + */
  33 +@Slf4j
  34 +public class IotModbusTcpClientProtocol implements IotProtocol {
  35 +
  36 + /**
  37 + * 协议配置
  38 + */
  39 + private final ProtocolProperties properties;
  40 + /**
  41 + * 服务器 ID(用于消息追踪,全局唯一)
  42 + */
  43 + @Getter
  44 + private final String serverId;
  45 +
  46 + /**
  47 + * 运行状态
  48 + */
  49 + @Getter
  50 + private volatile boolean running = false;
  51 +
  52 + /**
  53 + * Vert.x 实例
  54 + */
  55 + private final Vertx vertx;
  56 + /**
  57 + * 配置刷新定时器 ID
  58 + */
  59 + private Long configRefreshTimerId;
  60 +
  61 + /**
  62 + * 连接管理器
  63 + */
  64 + private final IotModbusTcpClientConnectionManager connectionManager;
  65 + /**
  66 + * 下行消息订阅者
  67 + */
  68 + private IotModbusTcpClientDownstreamSubscriber downstreamSubscriber;
  69 +
  70 + private final IotModbusTcpClientConfigCacheService configCacheService;
  71 + private final IotModbusTcpClientPollScheduler pollScheduler;
  72 +
  73 + public IotModbusTcpClientProtocol(ProtocolProperties properties) {
  74 + IotModbusTcpClientConfig modbusTcpClientConfig = properties.getModbusTcpClient();
  75 + Assert.notNull(modbusTcpClientConfig, "Modbus TCP Client 协议配置(modbusTcpClient)不能为空");
  76 + this.properties = properties;
  77 + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
  78 +
  79 + // 初始化 Vertx
  80 + this.vertx = Vertx.vertx();
  81 +
  82 + // 初始化 Manager
  83 + RedissonClient redissonClient = SpringUtil.getBean(RedissonClient.class);
  84 + IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
  85 + IotDeviceMessageService messageService = SpringUtil.getBean(IotDeviceMessageService.class);
  86 + this.configCacheService = new IotModbusTcpClientConfigCacheService(deviceApi);
  87 + this.connectionManager = new IotModbusTcpClientConnectionManager(redissonClient, vertx,
  88 + messageService, configCacheService, serverId);
  89 +
  90 + // 初始化 Handler
  91 + IotModbusTcpClientUpstreamHandler upstreamHandler = new IotModbusTcpClientUpstreamHandler(messageService, serverId);
  92 +
  93 + // 初始化轮询调度器
  94 + this.pollScheduler = new IotModbusTcpClientPollScheduler(vertx, connectionManager, upstreamHandler, configCacheService);
  95 + }
  96 +
  97 + @Override
  98 + public String getId() {
  99 + return properties.getId();
  100 + }
  101 +
  102 + @Override
  103 + public IotProtocolTypeEnum getType() {
  104 + return IotProtocolTypeEnum.MODBUS_TCP_CLIENT;
  105 + }
  106 +
  107 + @Override
  108 + public void start() {
  109 + if (running) {
  110 + log.warn("[start][IoT Modbus TCP Client 协议 {} 已经在运行中]", getId());
  111 + return;
  112 + }
  113 +
  114 + try {
  115 + // 1.1 首次加载配置
  116 + refreshConfig();
  117 + // 1.2 启动配置刷新定时器
  118 + int refreshInterval = properties.getModbusTcpClient().getConfigRefreshInterval();
  119 + configRefreshTimerId = vertx.setPeriodic(
  120 + TimeUnit.SECONDS.toMillis(refreshInterval),
  121 + id -> refreshConfig()
  122 + );
  123 + running = true;
  124 + log.info("[start][IoT Modbus TCP Client 协议 {} 启动成功,serverId={}]", getId(), serverId);
  125 +
  126 + // 2. 启动下行消息订阅者
  127 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
  128 + IotModbusTcpClientDownstreamHandler downstreamHandler = new IotModbusTcpClientDownstreamHandler(connectionManager,
  129 + configCacheService);
  130 + this.downstreamSubscriber = new IotModbusTcpClientDownstreamSubscriber(this, downstreamHandler, messageBus);
  131 + this.downstreamSubscriber.start();
  132 + } catch (Exception e) {
  133 + log.error("[start][IoT Modbus TCP Client 协议 {} 启动失败]", getId(), e);
  134 + stop0();
  135 + throw e;
  136 + }
  137 + }
  138 +
  139 + @Override
  140 + public void stop() {
  141 + if (!running) {
  142 + return;
  143 + }
  144 + stop0();
  145 + }
  146 +
  147 + private void stop0() {
  148 + // 1. 停止下行消息订阅者
  149 + if (downstreamSubscriber != null) {
  150 + try {
  151 + downstreamSubscriber.stop();
  152 + log.info("[stop][IoT Modbus TCP Client 协议 {} 下行消息订阅者已停止]", getId());
  153 + } catch (Exception e) {
  154 + log.error("[stop][IoT Modbus TCP Client 协议 {} 下行消息订阅者停止失败]", getId(), e);
  155 + }
  156 + downstreamSubscriber = null;
  157 + }
  158 +
  159 + // 2.1 取消配置刷新定时器
  160 + if (configRefreshTimerId != null) {
  161 + vertx.cancelTimer(configRefreshTimerId);
  162 + configRefreshTimerId = null;
  163 + }
  164 + // 2.2 停止轮询调度器
  165 + pollScheduler.stopAll();
  166 + // 2.3 关闭所有连接
  167 + connectionManager.closeAll();
  168 +
  169 + // 3. 关闭 Vert.x 实例
  170 + if (vertx != null) {
  171 + try {
  172 + vertx.close().result();
  173 + log.info("[stop][IoT Modbus TCP Client 协议 {} Vertx 已关闭]", getId());
  174 + } catch (Exception e) {
  175 + log.error("[stop][IoT Modbus TCP Client 协议 {} Vertx 关闭失败]", getId(), e);
  176 + }
  177 + }
  178 + running = false;
  179 + log.info("[stop][IoT Modbus TCP Client 协议 {} 已停止]", getId());
  180 + }
  181 +
  182 + /**
  183 + * 刷新配置
  184 + */
  185 + private synchronized void refreshConfig() {
  186 + try {
  187 + // 1. 从 biz 拉取最新配置(API 失败时返回 null)
  188 + List<IotModbusDeviceConfigRespDTO> configs = configCacheService.refreshConfig();
  189 + if (configs == null) {
  190 + log.warn("[refreshConfig][API 失败,跳过本轮刷新]");
  191 + return;
  192 + }
  193 + log.debug("[refreshConfig][获取到 {} 个 Modbus 设备配置]", configs.size());
  194 +
  195 + // 2. 更新连接和轮询任务
  196 + for (IotModbusDeviceConfigRespDTO config : configs) {
  197 + try {
  198 + // 2.1 确保连接存在
  199 + connectionManager.ensureConnection(config);
  200 + // 2.2 更新轮询任务
  201 + pollScheduler.updatePolling(config);
  202 + } catch (Exception e) {
  203 + log.error("[refreshConfig][处理设备配置失败, deviceId={}]", config.getDeviceId(), e);
  204 + }
  205 + }
  206 +
  207 + // 3. 清理已删除设备的资源
  208 + Set<Long> removedDeviceIds = configCacheService.cleanupRemovedDevices(configs);
  209 + for (Long deviceId : removedDeviceIds) {
  210 + pollScheduler.stopPolling(deviceId);
  211 + connectionManager.removeDevice(deviceId);
  212 + }
  213 + } catch (Exception e) {
  214 + log.error("[refreshConfig][刷新配置失败]", e);
  215 + }
  216 + }
  217 +
  218 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/handler/downstream/IotModbusTcpClientDownstreamHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.downstream;
  2 +
  3 +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.IotModbusPointRespDTO;
  6 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
  7 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  8 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
  9 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusTcpClientUtils;
  10 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConfigCacheService;
  11 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConnectionManager;
  12 +import lombok.RequiredArgsConstructor;
  13 +import lombok.extern.slf4j.Slf4j;
  14 +
  15 +import java.util.Map;
  16 +
  17 +/**
  18 + * IoT Modbus TCP Client 下行消息处理器
  19 + * <p>
  20 + * 负责:
  21 + * 1. 处理下行消息(如属性设置 thing.service.property.set)
  22 + * 2. 将属性值转换为 Modbus 写指令,通过 TCP 连接发送给设备
  23 + *
  24 + * @author 芋道源码
  25 + */
  26 +@RequiredArgsConstructor
  27 +@Slf4j
  28 +public class IotModbusTcpClientDownstreamHandler {
  29 +
  30 + private final IotModbusTcpClientConnectionManager connectionManager;
  31 + private final IotModbusTcpClientConfigCacheService configCacheService;
  32 +
  33 + /**
  34 + * 处理下行消息
  35 + */
  36 + @SuppressWarnings({"unchecked", "DuplicatedCode"})
  37 + public void handle(IotDeviceMessage message) {
  38 + // 1.1 检查是否是属性设置消息
  39 + if (ObjUtil.equals(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), message.getMethod())) {
  40 + return;
  41 + }
  42 + if (ObjUtil.notEqual(IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod(), message.getMethod())) {
  43 + log.warn("[handle][忽略非属性设置消息: {}]", message.getMethod());
  44 + return;
  45 + }
  46 + // 1.2 获取设备配置
  47 + IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(message.getDeviceId());
  48 + if (config == null) {
  49 + log.warn("[handle][设备 {} 没有 Modbus 配置]", message.getDeviceId());
  50 + return;
  51 + }
  52 +
  53 + // 2. 解析属性值并写入
  54 + Object params = message.getParams();
  55 + if (!(params instanceof Map)) {
  56 + log.warn("[handle][params 不是 Map 类型: {}]", params);
  57 + return;
  58 + }
  59 + Map<String, Object> propertyMap = (Map<String, Object>) params;
  60 + for (Map.Entry<String, Object> entry : propertyMap.entrySet()) {
  61 + String identifier = entry.getKey();
  62 + Object value = entry.getValue();
  63 + // 2.1 查找对应的点位配置
  64 + IotModbusPointRespDTO point = IotModbusCommonUtils.findPoint(config, identifier);
  65 + if (point == null) {
  66 + log.warn("[handle][设备 {} 没有点位配置: {}]", message.getDeviceId(), identifier);
  67 + continue;
  68 + }
  69 + // 2.2 检查是否支持写操作
  70 + if (!IotModbusCommonUtils.isWritable(point.getFunctionCode())) {
  71 + log.warn("[handle][点位 {} 不支持写操作, 功能码={}]", identifier, point.getFunctionCode());
  72 + continue;
  73 + }
  74 +
  75 + // 2.3 执行写入
  76 + writeProperty(config, point, value);
  77 + }
  78 + }
  79 +
  80 + /**
  81 + * 写入属性值
  82 + */
  83 + private void writeProperty(IotModbusDeviceConfigRespDTO config, IotModbusPointRespDTO point, Object value) {
  84 + // 1.1 获取连接
  85 + IotModbusTcpClientConnectionManager.ModbusConnection connection = connectionManager.getConnection(config.getDeviceId());
  86 + if (connection == null) {
  87 + log.warn("[writeProperty][设备 {} 没有连接]", config.getDeviceId());
  88 + return;
  89 + }
  90 + // 1.2 获取 slave ID
  91 + Integer slaveId = connectionManager.getSlaveId(config.getDeviceId());
  92 + if (slaveId == null) {
  93 + log.warn("[writeProperty][设备 {} 没有 slaveId]", config.getDeviceId());
  94 + return;
  95 + }
  96 +
  97 + // 2.1 转换属性值为原始值
  98 + int[] rawValues = IotModbusCommonUtils.convertToRawValues(value, point);
  99 + // 2.2 执行 Modbus 写入
  100 + IotModbusTcpClientUtils.write(connection, slaveId, point, rawValues)
  101 + .onSuccess(success -> log.info("[writeProperty][写入成功, deviceId={}, identifier={}, value={}]",
  102 + config.getDeviceId(), point.getIdentifier(), value))
  103 + .onFailure(e -> log.error("[writeProperty][写入失败, deviceId={}, identifier={}]",
  104 + config.getDeviceId(), point.getIdentifier(), e));
  105 + }
  106 +
  107 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/handler/downstream/IotModbusTcpClientDownstreamSubscriber.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.downstream;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
  4 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber;
  6 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.IotModbusTcpClientProtocol;
  7 +import lombok.extern.slf4j.Slf4j;
  8 +
  9 +/**
  10 + * IoT Modbus TCP 下行消息订阅器:订阅消息总线的下行消息并转发给处理器
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +@Slf4j
  15 +public class IotModbusTcpClientDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
  16 +
  17 + private final IotModbusTcpClientDownstreamHandler downstreamHandler;
  18 +
  19 + public IotModbusTcpClientDownstreamSubscriber(IotModbusTcpClientProtocol protocol,
  20 + IotModbusTcpClientDownstreamHandler downstreamHandler,
  21 + IotMessageBus messageBus) {
  22 + super(protocol, messageBus);
  23 + this.downstreamHandler = downstreamHandler;
  24 + }
  25 +
  26 + @Override
  27 + protected void handleMessage(IotDeviceMessage message) {
  28 + downstreamHandler.handle(message);
  29 + }
  30 +
  31 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/handler/upstream/IotModbusTcpClientUpstreamHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.upstream;
  2 +
  3 +import cn.hutool.core.map.MapUtil;
  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.core.enums.IotDeviceMessageMethodEnum;
  7 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  8 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
  9 +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  10 +import lombok.extern.slf4j.Slf4j;
  11 +
  12 +import java.util.Map;
  13 +
  14 +/**
  15 + * IoT Modbus TCP 上行数据处理器:将原始值转换为物模型属性值并上报
  16 + *
  17 + * @author 芋道源码
  18 + */
  19 +@Slf4j
  20 +public class IotModbusTcpClientUpstreamHandler {
  21 +
  22 + private final IotDeviceMessageService messageService;
  23 +
  24 + private final String serverId;
  25 +
  26 + public IotModbusTcpClientUpstreamHandler(IotDeviceMessageService messageService,
  27 + String serverId) {
  28 + this.messageService = messageService;
  29 + this.serverId = serverId;
  30 + }
  31 +
  32 + /**
  33 + * 处理 Modbus 读取结果
  34 + *
  35 + * @param config 设备配置
  36 + * @param point 点位配置
  37 + * @param rawValue 原始值(int 数组)
  38 + */
  39 + public void handleReadResult(IotModbusDeviceConfigRespDTO config,
  40 + IotModbusPointRespDTO point,
  41 + int[] rawValue) {
  42 + try {
  43 + // 1.1 转换原始值为物模型属性值(点位翻译)
  44 + Object convertedValue = IotModbusCommonUtils.convertToPropertyValue(rawValue, point);
  45 + log.debug("[handleReadResult][设备={}, 属性={}, 原始值={}, 转换值={}]",
  46 + config.getDeviceId(), point.getIdentifier(), rawValue, convertedValue);
  47 + // 1.2 构造属性上报消息
  48 + Map<String, Object> params = MapUtil.of(point.getIdentifier(), convertedValue);
  49 + IotDeviceMessage message = IotDeviceMessage.requestOf(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), params);
  50 +
  51 + // 2. 发送到消息总线
  52 + messageService.sendDeviceMessage(message, config.getProductKey(),
  53 + config.getDeviceName(), serverId);
  54 + } catch (Exception e) {
  55 + log.error("[handleReadResult][处理读取结果失败, deviceId={}, identifier={}]",
  56 + config.getDeviceId(), point.getIdentifier(), e);
  57 + }
  58 + }
  59 +
  60 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/manager/IotModbusTcpClientConfigCacheService.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager;
  2 +
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  4 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  5 +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
  6 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO;
  7 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
  8 +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusModeEnum;
  9 +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
  10 +import lombok.RequiredArgsConstructor;
  11 +import lombok.extern.slf4j.Slf4j;
  12 +
  13 +import java.util.HashSet;
  14 +import java.util.List;
  15 +import java.util.Map;
  16 +import java.util.Set;
  17 +import java.util.concurrent.ConcurrentHashMap;
  18 +
  19 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
  20 +
  21 +/**
  22 + * IoT Modbus TCP Client 配置缓存服务
  23 + *
  24 + * @author 芋道源码
  25 + */
  26 +@RequiredArgsConstructor
  27 +@Slf4j
  28 +public class IotModbusTcpClientConfigCacheService {
  29 +
  30 + private final IotDeviceCommonApi deviceApi;
  31 +
  32 + /**
  33 + * 配置缓存:deviceId -> 配置
  34 + */
  35 + private final Map<Long, IotModbusDeviceConfigRespDTO> configCache = new ConcurrentHashMap<>();
  36 +
  37 + /**
  38 + * 已知的设备 ID 集合(作用:用于检测已删除的设备)
  39 + *
  40 + * @see #cleanupRemovedDevices(List)
  41 + */
  42 + private final Set<Long> knownDeviceIds = ConcurrentHashMap.newKeySet();
  43 +
  44 + /**
  45 + * 刷新配置
  46 + *
  47 + * @return 最新的配置列表;API 失败时返回 null(调用方应跳过 cleanup)
  48 + */
  49 + public List<IotModbusDeviceConfigRespDTO> refreshConfig() {
  50 + try {
  51 + // 1. 从远程获取配置
  52 + CommonResult<List<IotModbusDeviceConfigRespDTO>> result = deviceApi.getModbusDeviceConfigList(
  53 + new IotModbusDeviceConfigListReqDTO().setStatus(CommonStatusEnum.ENABLE.getStatus())
  54 + .setMode(IotModbusModeEnum.POLLING.getMode()).setProtocolType(IotProtocolTypeEnum.MODBUS_TCP_CLIENT.getType()));
  55 + result.checkError();
  56 + List<IotModbusDeviceConfigRespDTO> configs = result.getData();
  57 +
  58 + // 2. 更新缓存(注意:不在这里更新 knownDeviceIds,由 cleanupRemovedDevices 统一管理)
  59 + for (IotModbusDeviceConfigRespDTO config : configs) {
  60 + configCache.put(config.getDeviceId(), config);
  61 + }
  62 + return configs;
  63 + } catch (Exception e) {
  64 + log.error("[refreshConfig][刷新配置失败]", e);
  65 + return null;
  66 + }
  67 + }
  68 +
  69 + /**
  70 + * 获取设备配置
  71 + *
  72 + * @param deviceId 设备 ID
  73 + * @return 配置
  74 + */
  75 + public IotModbusDeviceConfigRespDTO getConfig(Long deviceId) {
  76 + return configCache.get(deviceId);
  77 + }
  78 +
  79 + /**
  80 + * 计算已删除设备的 ID 集合,清理缓存,并更新已知设备 ID 集合
  81 + *
  82 + * @param currentConfigs 当前有效的配置列表
  83 + * @return 已删除的设备 ID 集合
  84 + */
  85 + public Set<Long> cleanupRemovedDevices(List<IotModbusDeviceConfigRespDTO> currentConfigs) {
  86 + // 1.1 获取当前有效的设备 ID
  87 + Set<Long> currentDeviceIds = convertSet(currentConfigs, IotModbusDeviceConfigRespDTO::getDeviceId);
  88 + // 1.2 找出已删除的设备(基于旧的 knownDeviceIds)
  89 + Set<Long> removedDeviceIds = new HashSet<>(knownDeviceIds);
  90 + removedDeviceIds.removeAll(currentDeviceIds);
  91 +
  92 + // 2. 清理已删除设备的缓存
  93 + for (Long deviceId : removedDeviceIds) {
  94 + log.info("[cleanupRemovedDevices][清理已删除设备: {}]", deviceId);
  95 + configCache.remove(deviceId);
  96 + }
  97 +
  98 + // 3. 更新已知设备 ID 集合为当前有效的设备 ID
  99 + knownDeviceIds.clear();
  100 + knownDeviceIds.addAll(currentDeviceIds);
  101 + return removedDeviceIds;
  102 + }
  103 +
  104 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/manager/IotModbusTcpClientConnectionManager.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager;
  2 +
  3 +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.mq.message.IotDeviceMessage;
  6 +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  7 +import com.ghgande.j2mod.modbus.net.TCPMasterConnection;
  8 +import io.vertx.core.Context;
  9 +import io.vertx.core.Future;
  10 +import io.vertx.core.Vertx;
  11 +import lombok.Data;
  12 +import lombok.extern.slf4j.Slf4j;
  13 +import org.redisson.api.RLock;
  14 +import org.redisson.api.RedissonClient;
  15 +
  16 +import java.net.InetAddress;
  17 +import java.util.ArrayList;
  18 +import java.util.List;
  19 +import java.util.Map;
  20 +import java.util.concurrent.ConcurrentHashMap;
  21 +
  22 +/**
  23 + * IoT Modbus TCP 连接管理器
  24 + * <p>
  25 + * 统一管理 Modbus TCP 连接:
  26 + * 1. 管理 TCP 连接(相同 ip:port 共用连接)
  27 + * 2. 分布式锁管理(连接级别),避免多节点重复创建连接
  28 + * 3. 连接重试和故障恢复
  29 + *
  30 + * @author 芋道源码
  31 + */
  32 +@Slf4j
  33 +public class IotModbusTcpClientConnectionManager {
  34 +
  35 + private static final String LOCK_KEY_PREFIX = "iot:modbus-tcp:connection:";
  36 +
  37 + private final RedissonClient redissonClient;
  38 + private final Vertx vertx;
  39 + private final IotDeviceMessageService messageService;
  40 + private final IotModbusTcpClientConfigCacheService configCacheService;
  41 + private final String serverId;
  42 +
  43 + /**
  44 + * 连接池:key = ip:port
  45 + */
  46 + private final Map<String, ModbusConnection> connectionPool = new ConcurrentHashMap<>();
  47 +
  48 + /**
  49 + * 设备 ID 到连接 key 的映射
  50 + */
  51 + private final Map<Long, String> deviceConnectionMap = new ConcurrentHashMap<>();
  52 +
  53 + public IotModbusTcpClientConnectionManager(RedissonClient redissonClient, Vertx vertx,
  54 + IotDeviceMessageService messageService,
  55 + IotModbusTcpClientConfigCacheService configCacheService,
  56 + String serverId) {
  57 + this.redissonClient = redissonClient;
  58 + this.vertx = vertx;
  59 + this.messageService = messageService;
  60 + this.configCacheService = configCacheService;
  61 + this.serverId = serverId;
  62 + }
  63 +
  64 + /**
  65 + * 确保连接存在
  66 + * <p>
  67 + * 首次建连成功时,直接发送设备上线消息
  68 + *
  69 + * @param config 设备配置
  70 + */
  71 + public void ensureConnection(IotModbusDeviceConfigRespDTO config) {
  72 + // 1.1 检查设备是否切换了 IP/端口,若是则先清理旧连接
  73 + String connectionKey = buildConnectionKey(config.getIp(), config.getPort());
  74 + String oldConnectionKey = deviceConnectionMap.get(config.getDeviceId());
  75 + if (oldConnectionKey != null && ObjUtil.notEqual(oldConnectionKey, connectionKey)) {
  76 + log.info("[ensureConnection][设备 {} IP/端口变更: {} -> {}, 清理旧连接]",
  77 + config.getDeviceId(), oldConnectionKey, connectionKey);
  78 + removeDevice(config.getDeviceId());
  79 + }
  80 + // 1.2 记录设备与连接的映射
  81 + deviceConnectionMap.put(config.getDeviceId(), connectionKey);
  82 +
  83 + // 2. 情况一:连接已存在,注册设备并发送上线消息
  84 + ModbusConnection connection = connectionPool.get(connectionKey);
  85 + if (connection != null) {
  86 + addDeviceAndOnline(connection, config);
  87 + return;
  88 + }
  89 +
  90 + // 3. 情况二:连接不存在,加分布式锁创建新连接
  91 + RLock lock = redissonClient.getLock(LOCK_KEY_PREFIX + connectionKey);
  92 + if (!lock.tryLock()) {
  93 + log.debug("[ensureConnection][获取锁失败, 由其他节点负责: {}]", connectionKey);
  94 + return;
  95 + }
  96 + try {
  97 + // 3.1 double-check:拿到锁后再次检查,避免并发创建重复连接
  98 + connection = connectionPool.get(connectionKey);
  99 + if (connection != null) {
  100 + addDeviceAndOnline(connection, config);
  101 + lock.unlock();
  102 + return;
  103 + }
  104 + // 3.2 创建新连接
  105 + connection = createConnection(config);
  106 + connection.setLock(lock);
  107 + connectionPool.put(connectionKey, connection);
  108 + log.info("[ensureConnection][创建 Modbus 连接成功: {}]", connectionKey);
  109 + // 3.3 注册设备并发送上线消息
  110 + addDeviceAndOnline(connection, config);
  111 + } catch (Exception e) {
  112 + log.error("[ensureConnection][创建 Modbus 连接失败: {}]", connectionKey, e);
  113 + // 建连失败,释放锁让其他节点可重试
  114 + lock.unlock();
  115 + }
  116 + }
  117 +
  118 + /**
  119 + * 创建 Modbus TCP 连接
  120 + */
  121 + private ModbusConnection createConnection(IotModbusDeviceConfigRespDTO config) throws Exception {
  122 + // 1. 创建 TCP 连接
  123 + TCPMasterConnection tcpConnection = new TCPMasterConnection(InetAddress.getByName(config.getIp()));
  124 + tcpConnection.setPort(config.getPort());
  125 + tcpConnection.setTimeout(config.getTimeout());
  126 + tcpConnection.connect();
  127 +
  128 + // 2. 创建 Modbus 连接对象
  129 + return new ModbusConnection()
  130 + .setConnectionKey(buildConnectionKey(config.getIp(), config.getPort()))
  131 + .setTcpConnection(tcpConnection).setContext(vertx.getOrCreateContext())
  132 + .setTimeout(config.getTimeout()).setRetryInterval(config.getRetryInterval());
  133 + }
  134 +
  135 + /**
  136 + * 获取连接
  137 + */
  138 + public ModbusConnection getConnection(Long deviceId) {
  139 + String connectionKey = deviceConnectionMap.get(deviceId);
  140 + if (connectionKey == null) {
  141 + return null;
  142 + }
  143 + return connectionPool.get(connectionKey);
  144 + }
  145 +
  146 + /**
  147 + * 获取设备的 slave ID
  148 + */
  149 + public Integer getSlaveId(Long deviceId) {
  150 + ModbusConnection connection = getConnection(deviceId);
  151 + if (connection == null) {
  152 + return null;
  153 + }
  154 + return connection.getSlaveId(deviceId);
  155 + }
  156 +
  157 + /**
  158 + * 移除设备
  159 + * <p>
  160 + * 移除时直接发送设备下线消息
  161 + */
  162 + public void removeDevice(Long deviceId) {
  163 + // 1.1 移除设备时,发送下线消息
  164 + sendOfflineMessage(deviceId);
  165 + // 1.2 移除设备引用
  166 + String connectionKey = deviceConnectionMap.remove(deviceId);
  167 + if (connectionKey == null) {
  168 + return;
  169 + }
  170 +
  171 + // 2.1 移除连接中的设备引用
  172 + ModbusConnection connection = connectionPool.get(connectionKey);
  173 + if (connection == null) {
  174 + return;
  175 + }
  176 + connection.removeDevice(deviceId);
  177 + // 2.2 如果没有设备引用了,关闭连接
  178 + if (connection.getDeviceCount() == 0) {
  179 + closeConnection(connectionKey);
  180 + }
  181 + }
  182 +
  183 + // ==================== 设备连接 & 上下线消息 ====================
  184 +
  185 + /**
  186 + * 注册设备到连接,并发送上线消息
  187 + */
  188 + private void addDeviceAndOnline(ModbusConnection connection,
  189 + IotModbusDeviceConfigRespDTO config) {
  190 + Integer previous = connection.addDevice(config.getDeviceId(), config.getSlaveId());
  191 + // 首次注册,发送上线消息
  192 + if (previous == null) {
  193 + sendOnlineMessage(config);
  194 + }
  195 + }
  196 +
  197 + /**
  198 + * 发送设备上线消息
  199 + */
  200 + private void sendOnlineMessage(IotModbusDeviceConfigRespDTO config) {
  201 + try {
  202 + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline();
  203 + messageService.sendDeviceMessage(onlineMessage,
  204 + config.getProductKey(), config.getDeviceName(), serverId);
  205 + } catch (Exception ex) {
  206 + log.error("[sendOnlineMessage][发送设备上线消息失败, deviceId={}]", config.getDeviceId(), ex);
  207 + }
  208 + }
  209 +
  210 + /**
  211 + * 发送设备下线消息
  212 + */
  213 + private void sendOfflineMessage(Long deviceId) {
  214 + IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(deviceId);
  215 + if (config == null) {
  216 + return;
  217 + }
  218 + try {
  219 + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline();
  220 + messageService.sendDeviceMessage(offlineMessage,
  221 + config.getProductKey(), config.getDeviceName(), serverId);
  222 + } catch (Exception ex) {
  223 + log.error("[sendOfflineMessage][发送设备下线消息失败, deviceId={}]", deviceId, ex);
  224 + }
  225 + }
  226 +
  227 + /**
  228 + * 关闭指定连接
  229 + */
  230 + private void closeConnection(String connectionKey) {
  231 + ModbusConnection connection = connectionPool.remove(connectionKey);
  232 + if (connection == null) {
  233 + return;
  234 + }
  235 +
  236 + try {
  237 + if (connection.getTcpConnection() != null) {
  238 + connection.getTcpConnection().close();
  239 + }
  240 + // 释放分布式锁,让其他节点可接管
  241 + RLock lock = connection.getLock();
  242 + if (lock != null && lock.isHeldByCurrentThread()) {
  243 + lock.unlock();
  244 + }
  245 + log.info("[closeConnection][关闭 Modbus 连接: {}]", connectionKey);
  246 + } catch (Exception e) {
  247 + log.error("[closeConnection][关闭连接失败: {}]", connectionKey, e);
  248 + }
  249 + }
  250 +
  251 + /**
  252 + * 关闭所有连接
  253 + */
  254 + public void closeAll() {
  255 + // 先复制再遍历,避免 closeConnection 中 remove 导致并发修改
  256 + List<String> connectionKeys = new ArrayList<>(connectionPool.keySet());
  257 + for (String connectionKey : connectionKeys) {
  258 + closeConnection(connectionKey);
  259 + }
  260 + deviceConnectionMap.clear();
  261 + }
  262 +
  263 + private String buildConnectionKey(String ip, Integer port) {
  264 + return ip + ":" + port;
  265 + }
  266 +
  267 + /**
  268 + * Modbus 连接信息
  269 + */
  270 + @Data
  271 + public static class ModbusConnection {
  272 +
  273 + private String connectionKey;
  274 + private TCPMasterConnection tcpConnection;
  275 + private Integer timeout;
  276 + private Integer retryInterval;
  277 + /**
  278 + * 设备 ID 到 slave ID 的映射
  279 + */
  280 + private final Map<Long, Integer> deviceSlaveMap = new ConcurrentHashMap<>();
  281 +
  282 + /**
  283 + * 分布式锁,锁住连接的创建和销毁,避免多节点重复连接同一从站
  284 + */
  285 + private RLock lock;
  286 +
  287 + /**
  288 + * Vert.x Context,用于 executeBlocking 执行 Modbus 操作,保证同一连接的操作串行执行
  289 + */
  290 + private Context context;
  291 +
  292 + public Integer addDevice(Long deviceId, Integer slaveId) {
  293 + return deviceSlaveMap.putIfAbsent(deviceId, slaveId);
  294 + }
  295 +
  296 + public void removeDevice(Long deviceId) {
  297 + deviceSlaveMap.remove(deviceId);
  298 + }
  299 +
  300 + public int getDeviceCount() {
  301 + return deviceSlaveMap.size();
  302 + }
  303 +
  304 + public Integer getSlaveId(Long deviceId) {
  305 + return deviceSlaveMap.get(deviceId);
  306 + }
  307 +
  308 + /**
  309 + * 执行 Modbus 读取操作(阻塞方式,在 Vert.x worker 线程执行)
  310 + */
  311 + public <T> Future<T> executeBlocking(java.util.function.Function<TCPMasterConnection, T> operation) {
  312 + // ordered=true 保证同一 Context 的操作串行执行,不同连接之间可并行
  313 + return context.executeBlocking(() -> operation.apply(tcpConnection), true);
  314 + }
  315 + }
  316 +
  317 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/manager/IotModbusTcpClientPollScheduler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.lang.Assert;
  5 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
  6 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
  7 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.manager.AbstractIotModbusPollScheduler;
  8 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
  9 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusTcpClientUtils;
  10 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.upstream.IotModbusTcpClientUpstreamHandler;
  11 +import io.vertx.core.Vertx;
  12 +import lombok.extern.slf4j.Slf4j;
  13 +
  14 +/**
  15 + * IoT Modbus TCP Client 轮询调度器:管理点位的轮询定时器,调度读取任务并上报结果
  16 + *
  17 + * @author 芋道源码
  18 + */
  19 +@Slf4j
  20 +public class IotModbusTcpClientPollScheduler extends AbstractIotModbusPollScheduler {
  21 +
  22 + private final IotModbusTcpClientConnectionManager connectionManager;
  23 + private final IotModbusTcpClientUpstreamHandler upstreamHandler;
  24 + private final IotModbusTcpClientConfigCacheService configCacheService;
  25 +
  26 + public IotModbusTcpClientPollScheduler(Vertx vertx,
  27 + IotModbusTcpClientConnectionManager connectionManager,
  28 + IotModbusTcpClientUpstreamHandler upstreamHandler,
  29 + IotModbusTcpClientConfigCacheService configCacheService) {
  30 + super(vertx);
  31 + this.connectionManager = connectionManager;
  32 + this.upstreamHandler = upstreamHandler;
  33 + this.configCacheService = configCacheService;
  34 + }
  35 +
  36 + // ========== 轮询执行 ==========
  37 +
  38 + /**
  39 + * 轮询单个点位
  40 + */
  41 + @Override
  42 + protected void pollPoint(Long deviceId, Long pointId) {
  43 + // 1.1 从 configCache 获取最新配置
  44 + IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(deviceId);
  45 + if (config == null || CollUtil.isEmpty(config.getPoints())) {
  46 + log.warn("[pollPoint][设备 {} 没有配置]", deviceId);
  47 + return;
  48 + }
  49 + // 1.2 查找点位
  50 + IotModbusPointRespDTO point = IotModbusCommonUtils.findPointById(config, pointId);
  51 + if (point == null) {
  52 + log.warn("[pollPoint][设备 {} 点位 {} 未找到]", deviceId, pointId);
  53 + return;
  54 + }
  55 +
  56 + // 2.1 获取连接
  57 + IotModbusTcpClientConnectionManager.ModbusConnection connection = connectionManager.getConnection(deviceId);
  58 + if (connection == null) {
  59 + log.warn("[pollPoint][设备 {} 没有连接]", deviceId);
  60 + return;
  61 + }
  62 + // 2.2 获取 slave ID
  63 + Integer slaveId = connectionManager.getSlaveId(deviceId);
  64 + Assert.notNull(slaveId, "设备 {} 没有配置 slaveId", deviceId);
  65 +
  66 + // 3. 执行 Modbus 读取
  67 + IotModbusTcpClientUtils.read(connection, slaveId, point)
  68 + .onSuccess(rawValue -> upstreamHandler.handleReadResult(config, point, rawValue))
  69 + .onFailure(e -> log.error("[pollPoint][读取点位失败, deviceId={}, identifier={}]",
  70 + deviceId, point.getIdentifier(), e));
  71 + }
  72 +
  73 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/package-info.java 0 → 100644
  1 +/**
  2 + * Modbus TCP Client(主站)协议:网关主动连接并轮询 Modbus 从站设备
  3 + * <p>
  4 + * 基于 j2mod 实现,支持 FC01-04 读、FC05/06/15/16 写,定时轮询 + 下发属性设置
  5 + */
  6 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient;
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerConfig.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver;
  2 +
  3 +import jakarta.validation.constraints.Max;
  4 +import jakarta.validation.constraints.Min;
  5 +import jakarta.validation.constraints.NotNull;
  6 +import lombok.Data;
  7 +
  8 +/**
  9 + * IoT Modbus TCP Server 协议配置
  10 + *
  11 + * @author 芋道源码
  12 + */
  13 +@Data
  14 +public class IotModbusTcpServerConfig {
  15 +
  16 + /**
  17 + * 配置刷新间隔(秒)
  18 + */
  19 + @NotNull(message = "配置刷新间隔不能为空")
  20 + @Min(value = 1, message = "配置刷新间隔不能小于 1 秒")
  21 + private Integer configRefreshInterval = 30;
  22 +
  23 + /**
  24 + * 自定义功能码(用于认证等扩展交互)
  25 + * Modbus 协议保留 65-72 给用户自定义,默认 65
  26 + */
  27 + @NotNull(message = "自定义功能码不能为空")
  28 + @Min(value = 65, message = "自定义功能码不能小于 65")
  29 + @Max(value = 72, message = "自定义功能码不能大于 72")
  30 + private Integer customFunctionCode = 65;
  31 +
  32 + /**
  33 + * Pending Request 超时时间(毫秒)
  34 + */
  35 + @NotNull(message = "请求超时时间不能为空")
  36 + private Integer requestTimeout = 5000;
  37 +
  38 + /**
  39 + * Pending Request 清理间隔(毫秒)
  40 + */
  41 + @NotNull(message = "请求清理间隔不能为空")
  42 + private Integer requestCleanupInterval = 10000;
  43 +
  44 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerProtocol.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.lang.Assert;
  5 +import cn.hutool.extra.spring.SpringUtil;
  6 +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
  7 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
  8 +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
  9 +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
  10 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  11 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
  12 +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
  13 +import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
  14 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameDecoder;
  15 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameEncoder;
  16 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.handler.downstream.IotModbusTcpServerDownstreamHandler;
  17 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.handler.downstream.IotModbusTcpServerDownstreamSubscriber;
  18 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.handler.upstream.IotModbusTcpServerUpstreamHandler;
  19 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConfigCacheService;
  20 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager;
  21 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager.ConnectionInfo;
  22 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerPendingRequestManager;
  23 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerPollScheduler;
  24 +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
  25 +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  26 +import io.vertx.core.Vertx;
  27 +import io.vertx.core.net.NetServer;
  28 +import io.vertx.core.net.NetServerOptions;
  29 +import io.vertx.core.net.NetSocket;
  30 +import io.vertx.core.parsetools.RecordParser;
  31 +import lombok.Getter;
  32 +import lombok.extern.slf4j.Slf4j;
  33 +
  34 +import java.util.List;
  35 +import java.util.Set;
  36 +import java.util.concurrent.TimeUnit;
  37 +import java.util.concurrent.atomic.AtomicInteger;
  38 +
  39 +/**
  40 + * IoT 网关 Modbus TCP Server 协议
  41 + * <p>
  42 + * 作为 TCP Server 接收设备主动连接:
  43 + * 1. 设备通过自定义功能码(FC 65)发送认证请求
  44 + * 2. 认证成功后,网关主动发送 Modbus 读请求,设备响应(云端轮询模式)
  45 + *
  46 + * @author 芋道源码
  47 + */
  48 +@Slf4j
  49 +public class IotModbusTcpServerProtocol implements IotProtocol {
  50 +
  51 + /**
  52 + * 协议配置
  53 + */
  54 + private final ProtocolProperties properties;
  55 + /**
  56 + * 服务器 ID(用于消息追踪,全局唯一)
  57 + */
  58 + @Getter
  59 + private final String serverId;
  60 +
  61 + /**
  62 + * 运行状态
  63 + */
  64 + @Getter
  65 + private volatile boolean running = false;
  66 +
  67 + /**
  68 + * Vert.x 实例
  69 + */
  70 + private final Vertx vertx;
  71 + /**
  72 + * TCP Server
  73 + */
  74 + private NetServer netServer;
  75 + /**
  76 + * 配置刷新定时器 ID
  77 + */
  78 + private Long configRefreshTimerId;
  79 + /**
  80 + * Pending Request 清理定时器 ID
  81 + */
  82 + private Long requestCleanupTimerId;
  83 +
  84 + /**
  85 + * 连接管理器
  86 + */
  87 + private final IotModbusTcpServerConnectionManager connectionManager;
  88 + /**
  89 + * 下行消息订阅者
  90 + */
  91 + private IotModbusTcpServerDownstreamSubscriber downstreamSubscriber;
  92 +
  93 + private final IotModbusFrameDecoder frameDecoder;
  94 + @SuppressWarnings("FieldCanBeLocal")
  95 + private final IotModbusFrameEncoder frameEncoder;
  96 +
  97 + private final IotModbusTcpServerConfigCacheService configCacheService;
  98 + private final IotModbusTcpServerPendingRequestManager pendingRequestManager;
  99 + private final IotModbusTcpServerUpstreamHandler upstreamHandler;
  100 + private final IotModbusTcpServerPollScheduler pollScheduler;
  101 + private final IotDeviceMessageService messageService;
  102 +
  103 + public IotModbusTcpServerProtocol(ProtocolProperties properties) {
  104 + IotModbusTcpServerConfig slaveConfig = properties.getModbusTcpServer();
  105 + Assert.notNull(slaveConfig, "Modbus TCP Server 协议配置(modbusTcpServer)不能为空");
  106 + this.properties = properties;
  107 + this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
  108 +
  109 + // 初始化 Vertx
  110 + this.vertx = Vertx.vertx();
  111 +
  112 + // 初始化 Manager
  113 + IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
  114 + this.connectionManager = new IotModbusTcpServerConnectionManager();
  115 + this.configCacheService = new IotModbusTcpServerConfigCacheService(deviceApi);
  116 + this.pendingRequestManager = new IotModbusTcpServerPendingRequestManager();
  117 +
  118 + // 初始化帧编解码器
  119 + this.frameDecoder = new IotModbusFrameDecoder(slaveConfig.getCustomFunctionCode());
  120 + this.frameEncoder = new IotModbusFrameEncoder(slaveConfig.getCustomFunctionCode());
  121 +
  122 + // 初始化共享事务 ID 自增器(PollScheduler 和 DownstreamHandler 共用,避免 transactionId 冲突)
  123 + AtomicInteger transactionIdCounter = new AtomicInteger(0);
  124 + // 初始化轮询调度器
  125 + this.pollScheduler = new IotModbusTcpServerPollScheduler(
  126 + vertx, connectionManager, frameEncoder, pendingRequestManager,
  127 + slaveConfig.getRequestTimeout(), transactionIdCounter, configCacheService);
  128 +
  129 + // 初始化 Handler
  130 + this.messageService = SpringUtil.getBean(IotDeviceMessageService.class);
  131 + IotDeviceService deviceService = SpringUtil.getBean(IotDeviceService.class);
  132 + this.upstreamHandler = new IotModbusTcpServerUpstreamHandler(
  133 + deviceApi, this.messageService, frameEncoder,
  134 + connectionManager, configCacheService, pendingRequestManager,
  135 + pollScheduler, deviceService, serverId);
  136 + }
  137 +
  138 + @Override
  139 + public String getId() {
  140 + return properties.getId();
  141 + }
  142 +
  143 + @Override
  144 + public IotProtocolTypeEnum getType() {
  145 + return IotProtocolTypeEnum.MODBUS_TCP_SERVER;
  146 + }
  147 +
  148 + @Override
  149 + public void start() {
  150 + if (running) {
  151 + log.warn("[start][IoT Modbus TCP Server 协议 {} 已经在运行中]", getId());
  152 + return;
  153 + }
  154 +
  155 + try {
  156 + // 1. 启动配置刷新定时器
  157 + IotModbusTcpServerConfig slaveConfig = properties.getModbusTcpServer();
  158 + configRefreshTimerId = vertx.setPeriodic(
  159 + TimeUnit.SECONDS.toMillis(slaveConfig.getConfigRefreshInterval()),
  160 + id -> refreshConfig());
  161 +
  162 + // 2.1 启动 TCP Server
  163 + startTcpServer();
  164 +
  165 + // 2.2 启动 PendingRequest 清理定时器
  166 + requestCleanupTimerId = vertx.setPeriodic(
  167 + slaveConfig.getRequestCleanupInterval(),
  168 + id -> pendingRequestManager.cleanupExpired());
  169 + running = true;
  170 + log.info("[start][IoT Modbus TCP Server 协议 {} 启动成功, serverId={}, port={}]",
  171 + getId(), serverId, properties.getPort());
  172 +
  173 + // 3. 启动下行消息订阅
  174 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
  175 + IotModbusTcpServerDownstreamHandler downstreamHandler = new IotModbusTcpServerDownstreamHandler(
  176 + connectionManager, configCacheService, frameEncoder, this.pollScheduler.getTransactionIdCounter());
  177 + this.downstreamSubscriber = new IotModbusTcpServerDownstreamSubscriber(
  178 + this, downstreamHandler, messageBus);
  179 + downstreamSubscriber.start();
  180 + } catch (Exception e) {
  181 + log.error("[start][IoT Modbus TCP Server 协议 {} 启动失败]", getId(), e);
  182 + stop0();
  183 + throw e;
  184 + }
  185 + }
  186 +
  187 + @Override
  188 + public void stop() {
  189 + if (!running) {
  190 + return;
  191 + }
  192 + stop0();
  193 + }
  194 +
  195 + private void stop0() {
  196 + // 1. 停止下行消息订阅
  197 + if (downstreamSubscriber != null) {
  198 + try {
  199 + downstreamSubscriber.stop();
  200 + } catch (Exception e) {
  201 + log.error("[stop][下行消息订阅器停止失败]", e);
  202 + }
  203 + downstreamSubscriber = null;
  204 + }
  205 +
  206 + // 2.1 取消定时器
  207 + if (configRefreshTimerId != null) {
  208 + vertx.cancelTimer(configRefreshTimerId);
  209 + configRefreshTimerId = null;
  210 + }
  211 + if (requestCleanupTimerId != null) {
  212 + vertx.cancelTimer(requestCleanupTimerId);
  213 + requestCleanupTimerId = null;
  214 + }
  215 + // 2.2 停止轮询
  216 + pollScheduler.stopAll();
  217 + // 2.3 清理 PendingRequest
  218 + pendingRequestManager.clear();
  219 + // 2.4 关闭所有连接
  220 + connectionManager.closeAll();
  221 + // 2.5 关闭 TCP Server
  222 + if (netServer != null) {
  223 + try {
  224 + netServer.close().result();
  225 + log.info("[stop][TCP Server 已关闭]");
  226 + } catch (Exception e) {
  227 + log.error("[stop][TCP Server 关闭失败]", e);
  228 + }
  229 + netServer = null;
  230 + }
  231 +
  232 + // 3. 关闭 Vertx
  233 + if (vertx != null) {
  234 + try {
  235 + vertx.close().result();
  236 + } catch (Exception e) {
  237 + log.error("[stop][Vertx 关闭失败]", e);
  238 + }
  239 + }
  240 + running = false;
  241 + log.info("[stop][IoT Modbus TCP Server 协议 {} 已停止]", getId());
  242 + }
  243 +
  244 + /**
  245 + * 启动 TCP Server
  246 + */
  247 + private void startTcpServer() {
  248 + // 1. 创建 TCP Server
  249 + NetServerOptions options = new NetServerOptions()
  250 + .setPort(properties.getPort());
  251 + netServer = vertx.createNetServer(options);
  252 +
  253 + // 2. 设置连接处理器
  254 + netServer.connectHandler(this::handleConnection);
  255 + try {
  256 + netServer.listen().toCompletionStage().toCompletableFuture().get();
  257 + log.info("[startTcpServer][TCP Server 启动成功, port={}]", properties.getPort());
  258 + } catch (Exception e) {
  259 + throw new RuntimeException("[startTcpServer][TCP Server 启动失败]", e);
  260 + }
  261 + }
  262 +
  263 + /**
  264 + * 处理新连接
  265 + */
  266 + private void handleConnection(NetSocket socket) {
  267 + log.info("[handleConnection][新连接, remoteAddress={}]", socket.remoteAddress());
  268 +
  269 + // 1. 创建 RecordParser 并设置为数据处理器
  270 + RecordParser recordParser = frameDecoder.createRecordParser((frame, frameFormat) -> {
  271 + // 【重要】帧处理分发,即消息处理
  272 + upstreamHandler.handleFrame(socket, frame, frameFormat);
  273 + });
  274 + socket.handler(recordParser);
  275 +
  276 + // 2.1 连接关闭处理
  277 + socket.closeHandler(v -> {
  278 + ConnectionInfo info = connectionManager.removeConnection(socket);
  279 + if (info == null || info.getDeviceId() == null) {
  280 + log.info("[handleConnection][未认证连接关闭, remoteAddress={}]", socket.remoteAddress());
  281 + return;
  282 + }
  283 + pollScheduler.stopPolling(info.getDeviceId());
  284 + pendingRequestManager.removeDevice(info.getDeviceId());
  285 + configCacheService.removeConfig(info.getDeviceId());
  286 + // 发送设备下线消息
  287 + try {
  288 + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline();
  289 + messageService.sendDeviceMessage(offlineMessage, info.getProductKey(), info.getDeviceName(), serverId);
  290 + } catch (Exception ex) {
  291 + log.error("[handleConnection][发送设备下线消息失败, deviceId={}]", info.getDeviceId(), ex);
  292 + }
  293 + log.info("[handleConnection][连接关闭, deviceId={}, remoteAddress={}]",
  294 + info.getDeviceId(), socket.remoteAddress());
  295 + });
  296 + // 2.2 异常处理
  297 + socket.exceptionHandler(e -> {
  298 + log.error("[handleConnection][连接异常, remoteAddress={}]", socket.remoteAddress(), e);
  299 + socket.close();
  300 + });
  301 + }
  302 +
  303 + /**
  304 + * 刷新已连接设备的配置(定时调用)
  305 + */
  306 + private synchronized void refreshConfig() {
  307 + try {
  308 + // 1. 只刷新已连接设备的配置
  309 + Set<Long> connectedDeviceIds = connectionManager.getConnectedDeviceIds();
  310 + if (CollUtil.isEmpty(connectedDeviceIds)) {
  311 + return;
  312 + }
  313 + List<IotModbusDeviceConfigRespDTO> configs =
  314 + configCacheService.refreshConnectedDeviceConfigList(connectedDeviceIds);
  315 + if (configs == null) {
  316 + log.warn("[refreshConfig][刷新配置失败,跳过本次刷新]");
  317 + return;
  318 + }
  319 + log.debug("[refreshConfig][刷新了 {} 个已连接设备的配置]", configs.size());
  320 +
  321 + // 2. 更新已连接设备的轮询任务
  322 + for (IotModbusDeviceConfigRespDTO config : configs) {
  323 + try {
  324 + pollScheduler.updatePolling(config);
  325 + } catch (Exception e) {
  326 + log.error("[refreshConfig][处理设备配置失败, deviceId={}]", config.getDeviceId(), e);
  327 + }
  328 + }
  329 + } catch (Exception e) {
  330 + log.error("[refreshConfig][刷新配置失败]", e);
  331 + }
  332 + }
  333 +
  334 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/codec/IotModbusFrame.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum;
  4 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
  5 +import lombok.Data;
  6 +import lombok.experimental.Accessors;
  7 +
  8 +/**
  9 + * IoT Modbus 统一帧数据模型(TCP/RTU 公用)
  10 + *
  11 + * @author 芋道源码
  12 + */
  13 +@Data
  14 +@Accessors(chain = true)
  15 +public class IotModbusFrame {
  16 +
  17 + /**
  18 + * 从站地址
  19 + */
  20 + private int slaveId;
  21 + /**
  22 + * 功能码
  23 + */
  24 + private int functionCode;
  25 + /**
  26 + * PDU 数据(不含 slaveId)
  27 + */
  28 + private byte[] pdu;
  29 + /**
  30 + * 事务标识符
  31 + * <p>
  32 + * 仅 {@link IotModbusFrameFormatEnum#MODBUS_TCP} 格式有值
  33 + */
  34 + private Integer transactionId;
  35 +
  36 + /**
  37 + * 异常码
  38 + * <p>
  39 + * 当功能码最高位为 1 时(异常响应),此字段存储异常码。
  40 + *
  41 + * @see IotModbusCommonUtils#FC_EXCEPTION_MASK
  42 + */
  43 + private Integer exceptionCode;
  44 +
  45 + /**
  46 + * 自定义功能码时的 JSON 字符串(用于 auth 认证等等)
  47 + */
  48 + private String customData;
  49 +
  50 + /**
  51 + * 是否异常响应(基于 exceptionCode 是否有值判断)
  52 + */
  53 + public boolean isException() {
  54 + return exceptionCode != null;
  55 + }
  56 +
  57 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/codec/IotModbusFrameDecoder.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum;
  4 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
  5 +import io.vertx.core.Handler;
  6 +import io.vertx.core.buffer.Buffer;
  7 +import io.vertx.core.parsetools.RecordParser;
  8 +import lombok.RequiredArgsConstructor;
  9 +import lombok.extern.slf4j.Slf4j;
  10 +
  11 +import java.nio.ByteBuffer;
  12 +import java.nio.ByteOrder;
  13 +import java.nio.charset.StandardCharsets;
  14 +import java.util.function.BiConsumer;
  15 +
  16 +/**
  17 + * IoT Modbus 帧解码器:集成 TCP 拆包 + 帧格式探测 + 帧解码,一条龙完成从 TCP 字节流到 IotModbusFrame 的转换。
  18 + * <p>
  19 + * 流程:
  20 + * 1. 首帧检测:读前 6 字节,判断 MODBUS_TCP(ProtocolId==0x0000 且 Length 合理)或 MODBUS_RTU
  21 + * 2. 检测后切换到对应的拆包 Handler,并将首包 6 字节通过 handleFirstBytes() 交给新 Handler 处理
  22 + * 3. 拆包完成后解码为 IotModbusFrame,通过回调返回
  23 + * - MODBUS_TCP:两阶段 RecordParser(MBAP length 字段驱动)
  24 + * - MODBUS_RTU:功能码驱动的状态机
  25 + *
  26 + * @author 芋道源码
  27 + */
  28 +@RequiredArgsConstructor
  29 +@Slf4j
  30 +public class IotModbusFrameDecoder {
  31 +
  32 + private static final Boolean REQUEST_MODE_DEFAULT = false;
  33 +
  34 + /**
  35 + * 自定义功能码
  36 + */
  37 + private final int customFunctionCode;
  38 +
  39 + /**
  40 + * 创建带自动帧格式检测的 RecordParser(默认响应模式)
  41 + *
  42 + * @param frameHandler 完整帧回调(解码后的 IotModbusFrame + 检测到的帧格式)
  43 + * @return RecordParser 实例
  44 + */
  45 + public RecordParser createRecordParser(BiConsumer<IotModbusFrame, IotModbusFrameFormatEnum> frameHandler) {
  46 + return createRecordParser(frameHandler, REQUEST_MODE_DEFAULT);
  47 + }
  48 +
  49 + /**
  50 + * 创建带自动帧格式检测的 RecordParser
  51 + *
  52 + * @param frameHandler 完整帧回调(解码后的 IotModbusFrame + 检测到的帧格式)
  53 + * @param requestMode 是否为请求模式(true:接收方收到的是 Modbus 请求帧,FC01-04 按固定 8 字节解析;
  54 + * false:接收方收到的是 Modbus 响应帧,FC01-04 按 byteCount 变长解析)
  55 + * @return RecordParser 实例
  56 + */
  57 + public RecordParser createRecordParser(BiConsumer<IotModbusFrame, IotModbusFrameFormatEnum> frameHandler,
  58 + boolean requestMode) {
  59 + // 先创建一个 RecordParser:使用 fixedSizeMode(6) 读取首帧前 6 字节进行帧格式检测
  60 + RecordParser parser = RecordParser.newFixed(6);
  61 + parser.handler(new DetectPhaseHandler(parser, customFunctionCode, frameHandler, requestMode));
  62 + return parser;
  63 + }
  64 +
  65 + // ==================== 帧解码 ====================
  66 +
  67 + /**
  68 + * 解码响应帧(拆包后的完整帧 byte[])
  69 + *
  70 + * @param data 完整帧字节数组
  71 + * @param format 帧格式
  72 + * @return 解码后的 IotModbusFrame
  73 + */
  74 + private IotModbusFrame decodeResponse(byte[] data, IotModbusFrameFormatEnum format) {
  75 + if (format == IotModbusFrameFormatEnum.MODBUS_TCP) {
  76 + return decodeTcpResponse(data);
  77 + } else {
  78 + return decodeRtuResponse(data);
  79 + }
  80 + }
  81 +
  82 + /**
  83 + * 解码 MODBUS_TCP 响应
  84 + * 格式:[TransactionId(2)] [ProtocolId(2)] [Length(2)] [UnitId(1)] [FC(1)] [Data...]
  85 + */
  86 + private IotModbusFrame decodeTcpResponse(byte[] data) {
  87 + if (data.length < 8) {
  88 + log.warn("[decodeTcpResponse][数据长度不足: {}]", data.length);
  89 + return null;
  90 + }
  91 + ByteBuffer buf = ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN);
  92 + int transactionId = buf.getShort() & 0xFFFF;
  93 + buf.getShort(); // protocolId:固定 0x0000,Modbus 协议标识
  94 + buf.getShort(); // length:后续字节数(UnitId + PDU),拆包阶段已使用
  95 + int slaveId = buf.get() & 0xFF;
  96 + int functionCode = buf.get() & 0xFF;
  97 + // 提取 PDU 数据(从 functionCode 之后到末尾)
  98 + byte[] pdu = new byte[data.length - 8];
  99 + System.arraycopy(data, 8, pdu, 0, pdu.length);
  100 + // 构建 IotModbusFrame
  101 + return buildFrame(slaveId, functionCode, pdu, transactionId);
  102 + }
  103 +
  104 + /**
  105 + * 解码 MODBUS_RTU 响应
  106 + * 格式:[SlaveId(1)] [FC(1)] [Data...] [CRC(2)]
  107 + */
  108 + private IotModbusFrame decodeRtuResponse(byte[] data) {
  109 + if (data.length < 4) {
  110 + log.warn("[decodeRtuResponse][数据长度不足: {}]", data.length);
  111 + return null;
  112 + }
  113 + // 校验 CRC
  114 + if (!IotModbusCommonUtils.verifyCrc16(data)) {
  115 + log.warn("[decodeRtuResponse][CRC 校验失败]");
  116 + return null;
  117 + }
  118 + int slaveId = data[0] & 0xFF;
  119 + int functionCode = data[1] & 0xFF;
  120 + // PDU 数据(不含 slaveId、functionCode、CRC)
  121 + byte[] pdu = new byte[data.length - 4];
  122 + System.arraycopy(data, 2, pdu, 0, pdu.length);
  123 + // 构建 IotModbusFrame
  124 + return buildFrame(slaveId, functionCode, pdu, null);
  125 + }
  126 +
  127 + /**
  128 + * 构建 IotModbusFrame
  129 + */
  130 + private IotModbusFrame buildFrame(int slaveId, int functionCode, byte[] pdu, Integer transactionId) {
  131 + IotModbusFrame frame = new IotModbusFrame()
  132 + .setSlaveId(slaveId)
  133 + .setFunctionCode(functionCode)
  134 + .setPdu(pdu)
  135 + .setTransactionId(transactionId);
  136 + // 异常响应
  137 + if (IotModbusCommonUtils.isExceptionResponse(functionCode)) {
  138 + frame.setFunctionCode(IotModbusCommonUtils.extractOriginalFunctionCode(functionCode));
  139 + if (pdu.length >= 1) {
  140 + frame.setExceptionCode(pdu[0] & 0xFF);
  141 + }
  142 + return frame;
  143 + }
  144 + // 自定义功能码
  145 + if (functionCode == customFunctionCode) {
  146 + // data 区格式:[byteCount(1)] [JSON data(N)]
  147 + if (pdu.length >= 1) {
  148 + int byteCount = pdu[0] & 0xFF;
  149 + if (pdu.length >= 1 + byteCount) {
  150 + frame.setCustomData(new String(pdu, 1, byteCount, StandardCharsets.UTF_8));
  151 + }
  152 + }
  153 + }
  154 + return frame;
  155 + }
  156 +
  157 + // ==================== 拆包 Handler ====================
  158 +
  159 + /**
  160 + * 帧格式检测阶段 Handler(仅处理首包,探测后切换到对应的拆包 Handler)
  161 + */
  162 + @RequiredArgsConstructor
  163 + private class DetectPhaseHandler implements Handler<Buffer> {
  164 +
  165 + private final RecordParser parser;
  166 + private final int customFunctionCode;
  167 + private final BiConsumer<IotModbusFrame, IotModbusFrameFormatEnum> frameHandler;
  168 + private final boolean requestMode;
  169 +
  170 + @Override
  171 + public void handle(Buffer buffer) {
  172 + // 检测帧格式:protocolId==0x0000 且 length 合法 → MODBUS_TCP,否则 → MODBUS_RTU
  173 + byte[] bytes = buffer.getBytes();
  174 + int protocolId = ((bytes[2] & 0xFF) << 8) | (bytes[3] & 0xFF);
  175 + int length = ((bytes[4] & 0xFF) << 8) | (bytes[5] & 0xFF);
  176 +
  177 + // 分别处理 MODBUS_TCP、MODBUS_RTU 两种情况
  178 + if (protocolId == 0x0000 && length >= 1 && length <= 253) {
  179 + // MODBUS_TCP:切换到 TCP 拆包 Handler
  180 + log.debug("[DetectPhaseHandler][检测到 MODBUS_TCP 帧格式]");
  181 + TcpFrameHandler tcpHandler = new TcpFrameHandler(parser, frameHandler);
  182 + parser.handler(tcpHandler);
  183 + // 当前 bytes 就是 MBAP 的前 6 字节,直接交给 tcpHandler 处理
  184 + tcpHandler.handleFirstBytes(bytes);
  185 + } else {
  186 + // MODBUS_RTU:切换到 RTU 拆包 Handler
  187 + log.debug("[DetectPhaseHandler][检测到 MODBUS_RTU 帧格式]");
  188 + RtuFrameHandler rtuHandler = new RtuFrameHandler(parser, frameHandler, customFunctionCode, requestMode);
  189 + parser.handler(rtuHandler);
  190 + // 当前 bytes 包含前 6 字节(slaveId + FC + 部分数据),交给 rtuHandler 处理
  191 + rtuHandler.handleFirstBytes(bytes);
  192 + }
  193 + }
  194 + }
  195 +
  196 + /**
  197 + * MODBUS_TCP 拆包 Handler(两阶段 RecordParser)
  198 + * <p>
  199 + * Phase 1: fixedSizeMode(6) → 读 MBAP 前 6 字节,提取 length
  200 + * Phase 2: fixedSizeMode(length) → 读 unitId + PDU
  201 + */
  202 + @RequiredArgsConstructor
  203 + private class TcpFrameHandler implements Handler<Buffer> {
  204 +
  205 + private final RecordParser parser;
  206 + private final BiConsumer<IotModbusFrame, IotModbusFrameFormatEnum> frameHandler;
  207 +
  208 + private byte[] mbapHeader;
  209 + private boolean waitingForBody = false;
  210 +
  211 + /**
  212 + * 处理探测阶段传来的首帧 6 字节(即 MBAP 头)
  213 + *
  214 + * @param bytes 探测阶段消费的 6 字节
  215 + */
  216 + void handleFirstBytes(byte[] bytes) {
  217 + int length = ((bytes[4] & 0xFF) << 8) | (bytes[5] & 0xFF);
  218 + this.mbapHeader = bytes;
  219 + this.waitingForBody = true;
  220 + parser.fixedSizeMode(length);
  221 + }
  222 +
  223 + @Override
  224 + public void handle(Buffer buffer) {
  225 + if (waitingForBody) {
  226 + // Phase 2: 收到 body(unitId + PDU)
  227 + byte[] body = buffer.getBytes();
  228 + // 拼接完整帧:MBAP(6) + body
  229 + byte[] fullFrame = new byte[mbapHeader.length + body.length];
  230 + System.arraycopy(mbapHeader, 0, fullFrame, 0, mbapHeader.length);
  231 + System.arraycopy(body, 0, fullFrame, mbapHeader.length, body.length);
  232 + // 解码并回调
  233 + IotModbusFrame frame = decodeResponse(fullFrame, IotModbusFrameFormatEnum.MODBUS_TCP);
  234 + if (frame != null) {
  235 + frameHandler.accept(frame, IotModbusFrameFormatEnum.MODBUS_TCP);
  236 + }
  237 + // 切回 Phase 1
  238 + waitingForBody = false;
  239 + mbapHeader = null;
  240 + parser.fixedSizeMode(6);
  241 + } else {
  242 + // Phase 1: 收到 MBAP 头 6 字节
  243 + byte[] header = buffer.getBytes();
  244 + int length = ((header[4] & 0xFF) << 8) | (header[5] & 0xFF);
  245 + if (length < 1 || length > 253) {
  246 + log.warn("[TcpFrameHandler][MBAP Length 异常: {}]", length);
  247 + parser.fixedSizeMode(6);
  248 + return;
  249 + }
  250 + this.mbapHeader = header;
  251 + this.waitingForBody = true;
  252 + parser.fixedSizeMode(length);
  253 + }
  254 + }
  255 + }
  256 +
  257 + /**
  258 + * MODBUS_RTU 拆包 Handler(功能码驱动的状态机)
  259 + * <p>
  260 + * 状态机流程:
  261 + * Phase 1: fixedSizeMode(2) → 读 slaveId + functionCode
  262 + * Phase 2: 根据 functionCode 确定剩余长度:
  263 + * - 异常响应 (FC & EXCEPTION_MASK):fixedSizeMode(3) → exceptionCode(1) + CRC(2)
  264 + * - 自定义 FC / FC01-04 响应:fixedSizeMode(1) → 读 byteCount → fixedSizeMode(byteCount + 2)
  265 + * - FC05/06 响应:fixedSizeMode(6) → addr(2) + value(2) + CRC(2)
  266 + * - FC15/16 响应:fixedSizeMode(6) → addr(2) + quantity(2) + CRC(2)
  267 + * <p>
  268 + * 请求模式(requestMode=true)时,FC01-04 按固定 8 字节解析(与写响应相同路径),
  269 + * 因为读请求格式为 [SlaveId(1)][FC(1)][StartAddr(2)][Quantity(2)][CRC(2)]
  270 + */
  271 + @RequiredArgsConstructor
  272 + private class RtuFrameHandler implements Handler<Buffer> {
  273 +
  274 + private static final int STATE_HEADER = 0;
  275 + private static final int STATE_EXCEPTION_BODY = 1;
  276 + private static final int STATE_READ_BYTE_COUNT = 2;
  277 + private static final int STATE_READ_DATA = 3;
  278 + private static final int STATE_WRITE_BODY = 4;
  279 +
  280 + private final RecordParser parser;
  281 + private final BiConsumer<IotModbusFrame, IotModbusFrameFormatEnum> frameHandler;
  282 + private final int customFunctionCode;
  283 + /**
  284 + * 请求模式:
  285 + * - true 表示接收方收到的是 Modbus 请求帧(如设备端收到网关下发的读请求),FC01-04 按固定 8 字节帧解析
  286 + * - false 表示接收方收到的是 Modbus 响应帧,FC01-04 按 byteCount 变长解析
  287 + */
  288 + private final boolean requestMode;
  289 +
  290 + private int state = STATE_HEADER;
  291 + private byte slaveId;
  292 + private byte functionCode;
  293 + private byte byteCount;
  294 + private Buffer pendingData;
  295 + private int expectedDataLen;
  296 +
  297 + /**
  298 + * 处理探测阶段传来的首帧 6 字节
  299 + * <p>
  300 + * 由于 RTU 首帧被探测阶段消费了 6 字节,这里需要从中提取 slaveId + FC 并根据 FC 处理剩余数据
  301 + *
  302 + * @param bytes 探测阶段消费的 6 字节:[slaveId][FC][...4 bytes...]
  303 + */
  304 + void handleFirstBytes(byte[] bytes) {
  305 + this.slaveId = bytes[0];
  306 + this.functionCode = bytes[1];
  307 + int fc = functionCode & 0xFF;
  308 + if (IotModbusCommonUtils.isExceptionResponse(fc)) {
  309 + // 异常响应:完整帧 = slaveId(1) + FC(1) + exceptionCode(1) + CRC(2) = 5 字节
  310 + // 已有 6 字节(多 1 字节),取前 5 字节组装
  311 + Buffer frame = Buffer.buffer(5);
  312 + frame.appendByte(slaveId);
  313 + frame.appendByte(functionCode);
  314 + frame.appendBytes(bytes, 2, 3); // exceptionCode + CRC
  315 + emitFrame(frame);
  316 + resetToHeader();
  317 + } else if (IotModbusCommonUtils.isReadResponse(fc) && requestMode) {
  318 + // 请求模式下的读请求:固定 8 字节 [SlaveId(1)][FC(1)][StartAddr(2)][Quantity(2)][CRC(2)]
  319 + // 已有 6 字节,还需 2 字节(CRC)
  320 + state = STATE_WRITE_BODY;
  321 + this.pendingData = Buffer.buffer();
  322 + this.pendingData.appendBytes(bytes, 2, 4); // 暂存已有的 4 字节(StartAddr + Quantity)
  323 + parser.fixedSizeMode(2); // 还需 2 字节(CRC)
  324 + } else if (IotModbusCommonUtils.isReadResponse(fc) || fc == customFunctionCode) {
  325 + // 读响应或自定义 FC:bytes[2] = byteCount
  326 + this.byteCount = bytes[2];
  327 + int bc = byteCount & 0xFF;
  328 + // 已有数据:bytes[3..5] = 3 字节
  329 + // 还需:byteCount + CRC(2) - 3 字节已有
  330 + int remaining = bc + 2 - 3;
  331 + if (remaining <= 0) {
  332 + // 数据已足够,组装完整帧
  333 + int totalLen = 2 + 1 + bc + 2; // slaveId + FC + byteCount + data + CRC
  334 + Buffer frame = Buffer.buffer(totalLen);
  335 + frame.appendByte(slaveId);
  336 + frame.appendByte(functionCode);
  337 + frame.appendByte(byteCount);
  338 + frame.appendBytes(bytes, 3, bc + 2); // data + CRC
  339 + emitFrame(frame);
  340 + resetToHeader();
  341 + } else {
  342 + // 需要继续读
  343 + state = STATE_READ_DATA;
  344 + this.pendingData = Buffer.buffer();
  345 + this.pendingData.appendBytes(bytes, 3, 3); // 暂存已有的 3 字节
  346 + this.expectedDataLen = bc + 2; // byteCount 个数据 + 2 CRC
  347 + parser.fixedSizeMode(remaining);
  348 + }
  349 + } else if (IotModbusCommonUtils.isWriteResponse(fc)) {
  350 + // 写响应:总长 = slaveId(1) + FC(1) + addr(2) + value/qty(2) + CRC(2) = 8 字节
  351 + // 已有 6 字节,还需 2 字节
  352 + state = STATE_WRITE_BODY;
  353 + this.pendingData = Buffer.buffer();
  354 + this.pendingData.appendBytes(bytes, 2, 4); // 暂存已有的 4 字节
  355 + parser.fixedSizeMode(2); // 还需 2 字节(CRC)
  356 + } else {
  357 + log.warn("[RtuFrameHandler][未知功能码: 0x{}]", Integer.toHexString(fc));
  358 + resetToHeader();
  359 + }
  360 + }
  361 +
  362 + @Override
  363 + public void handle(Buffer buffer) {
  364 + switch (state) {
  365 + case STATE_HEADER:
  366 + handleHeader(buffer);
  367 + break;
  368 + case STATE_EXCEPTION_BODY:
  369 + handleExceptionBody(buffer);
  370 + break;
  371 + case STATE_READ_BYTE_COUNT:
  372 + handleReadByteCount(buffer);
  373 + break;
  374 + case STATE_READ_DATA:
  375 + handleReadData(buffer);
  376 + break;
  377 + case STATE_WRITE_BODY:
  378 + handleWriteBody(buffer);
  379 + break;
  380 + default:
  381 + resetToHeader();
  382 + }
  383 + }
  384 +
  385 + private void handleHeader(Buffer buffer) {
  386 + byte[] header = buffer.getBytes();
  387 + this.slaveId = header[0];
  388 + this.functionCode = header[1];
  389 + int fc = functionCode & 0xFF;
  390 + if (IotModbusCommonUtils.isExceptionResponse(fc)) {
  391 + // 异常响应
  392 + state = STATE_EXCEPTION_BODY;
  393 + parser.fixedSizeMode(3); // exceptionCode(1) + CRC(2)
  394 + } else if (IotModbusCommonUtils.isReadResponse(fc) && requestMode) {
  395 + // 请求模式下的读请求:固定 8 字节,已读 2 字节(slaveId + FC),还需 6 字节
  396 + state = STATE_WRITE_BODY;
  397 + pendingData = Buffer.buffer();
  398 + parser.fixedSizeMode(6); // StartAddr(2) + Quantity(2) + CRC(2)
  399 + } else if (IotModbusCommonUtils.isReadResponse(fc) || fc == customFunctionCode) {
  400 + // 读响应或自定义 FC
  401 + state = STATE_READ_BYTE_COUNT;
  402 + parser.fixedSizeMode(1); // byteCount
  403 + } else if (IotModbusCommonUtils.isWriteResponse(fc)) {
  404 + // 写响应
  405 + state = STATE_WRITE_BODY;
  406 + pendingData = Buffer.buffer();
  407 + parser.fixedSizeMode(6); // addr(2) + value(2) + CRC(2)
  408 + } else {
  409 + log.warn("[RtuFrameHandler][未知功能码: 0x{}]", Integer.toHexString(fc));
  410 + resetToHeader();
  411 + }
  412 + }
  413 +
  414 + private void handleExceptionBody(Buffer buffer) {
  415 + // buffer = exceptionCode(1) + CRC(2)
  416 + Buffer frame = Buffer.buffer();
  417 + frame.appendByte(slaveId);
  418 + frame.appendByte(functionCode);
  419 + frame.appendBuffer(buffer);
  420 + emitFrame(frame);
  421 + resetToHeader();
  422 + }
  423 +
  424 + private void handleReadByteCount(Buffer buffer) {
  425 + this.byteCount = buffer.getByte(0);
  426 + int bc = byteCount & 0xFF;
  427 + state = STATE_READ_DATA;
  428 + pendingData = Buffer.buffer();
  429 + expectedDataLen = bc + 2; // data(bc) + CRC(2)
  430 + parser.fixedSizeMode(expectedDataLen);
  431 + }
  432 +
  433 + private void handleReadData(Buffer buffer) {
  434 + pendingData.appendBuffer(buffer);
  435 + if (pendingData.length() >= expectedDataLen) {
  436 + // 组装完整帧
  437 + Buffer frame = Buffer.buffer();
  438 + frame.appendByte(slaveId);
  439 + frame.appendByte(functionCode);
  440 + frame.appendByte(byteCount);
  441 + frame.appendBuffer(pendingData);
  442 + emitFrame(frame);
  443 + resetToHeader();
  444 + }
  445 + // 否则继续等待(不应该发生,因为我们精确设置了 fixedSizeMode)
  446 + }
  447 +
  448 + private void handleWriteBody(Buffer buffer) {
  449 + pendingData.appendBuffer(buffer);
  450 + // 完整帧
  451 + Buffer frame = Buffer.buffer();
  452 + frame.appendByte(slaveId);
  453 + frame.appendByte(functionCode);
  454 + frame.appendBuffer(pendingData);
  455 + emitFrame(frame);
  456 + resetToHeader();
  457 + }
  458 +
  459 + /**
  460 + * 发射完整帧:解码并回调
  461 + */
  462 + private void emitFrame(Buffer frameBuffer) {
  463 + IotModbusFrame frame = decodeResponse(frameBuffer.getBytes(), IotModbusFrameFormatEnum.MODBUS_RTU);
  464 + if (frame != null) {
  465 + frameHandler.accept(frame, IotModbusFrameFormatEnum.MODBUS_RTU);
  466 + }
  467 + }
  468 +
  469 + private void resetToHeader() {
  470 + state = STATE_HEADER;
  471 + pendingData = null;
  472 + parser.fixedSizeMode(2); // slaveId + FC
  473 + }
  474 +
  475 + }
  476 +
  477 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/codec/IotModbusFrameEncoder.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum;
  4 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
  5 +import lombok.RequiredArgsConstructor;
  6 +import lombok.extern.slf4j.Slf4j;
  7 +
  8 +import java.nio.charset.StandardCharsets;
  9 +
  10 +/**
  11 + * IoT Modbus 帧编码器:负责将 Modbus 请求/响应编码为字节数组,支持 MODBUS_TCP(MBAP)和 MODBUS_RTU(CRC16)两种帧格式。
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +@RequiredArgsConstructor
  16 +@Slf4j
  17 +public class IotModbusFrameEncoder {
  18 +
  19 + private final int customFunctionCode;
  20 +
  21 + // ==================== 编码 ====================
  22 +
  23 + /**
  24 + * 编码读请求
  25 + *
  26 + * @param slaveId 从站地址
  27 + * @param functionCode 功能码
  28 + * @param startAddress 起始寄存器地址
  29 + * @param quantity 寄存器数量
  30 + * @param format 帧格式
  31 + * @param transactionId 事务 ID(TCP 模式下使用,RTU 模式传 null)
  32 + * @return 编码后的字节数组
  33 + */
  34 + public byte[] encodeReadRequest(int slaveId, int functionCode, int startAddress, int quantity,
  35 + IotModbusFrameFormatEnum format, Integer transactionId) {
  36 + // PDU: [FC(1)] [StartAddress(2)] [Quantity(2)]
  37 + byte[] pdu = new byte[5];
  38 + pdu[0] = (byte) functionCode;
  39 + pdu[1] = (byte) ((startAddress >> 8) & 0xFF);
  40 + pdu[2] = (byte) (startAddress & 0xFF);
  41 + pdu[3] = (byte) ((quantity >> 8) & 0xFF);
  42 + pdu[4] = (byte) (quantity & 0xFF);
  43 + return wrapFrame(slaveId, pdu, format, transactionId);
  44 + }
  45 +
  46 + /**
  47 + * 编码写请求(单个寄存器 FC06 / 单个线圈 FC05)
  48 + *
  49 + * @param slaveId 从站地址
  50 + * @param functionCode 功能码
  51 + * @param address 寄存器地址
  52 + * @param value 值
  53 + * @param format 帧格式
  54 + * @param transactionId 事务 ID(TCP 模式下使用,RTU 模式传 null)
  55 + * @return 编码后的字节数组
  56 + */
  57 + public byte[] encodeWriteSingleRequest(int slaveId, int functionCode, int address, int value,
  58 + IotModbusFrameFormatEnum format, Integer transactionId) {
  59 + // FC05 单写线圈:Modbus 标准要求 value 为 0xFF00(ON)或 0x0000(OFF)
  60 + if (functionCode == IotModbusCommonUtils.FC_WRITE_SINGLE_COIL) {
  61 + value = (value != 0) ? 0xFF00 : 0x0000;
  62 + }
  63 + // PDU: [FC(1)] [Address(2)] [Value(2)]
  64 + byte[] pdu = new byte[5];
  65 + pdu[0] = (byte) functionCode;
  66 + pdu[1] = (byte) ((address >> 8) & 0xFF);
  67 + pdu[2] = (byte) (address & 0xFF);
  68 + pdu[3] = (byte) ((value >> 8) & 0xFF);
  69 + pdu[4] = (byte) (value & 0xFF);
  70 + return wrapFrame(slaveId, pdu, format, transactionId);
  71 + }
  72 +
  73 + /**
  74 + * 编码写多个寄存器请求(FC16)
  75 + *
  76 + * @param slaveId 从站地址
  77 + * @param address 起始地址
  78 + * @param values 值数组
  79 + * @param format 帧格式
  80 + * @param transactionId 事务 ID(TCP 模式下使用,RTU 模式传 null)
  81 + * @return 编码后的字节数组
  82 + */
  83 + public byte[] encodeWriteMultipleRegistersRequest(int slaveId, int address, int[] values,
  84 + IotModbusFrameFormatEnum format, Integer transactionId) {
  85 + // PDU: [FC(1)] [Address(2)] [Quantity(2)] [ByteCount(1)] [Values(N*2)]
  86 + int quantity = values.length;
  87 + int byteCount = quantity * 2;
  88 + byte[] pdu = new byte[6 + byteCount];
  89 + pdu[0] = (byte) 16; // FC16
  90 + pdu[1] = (byte) ((address >> 8) & 0xFF);
  91 + pdu[2] = (byte) (address & 0xFF);
  92 + pdu[3] = (byte) ((quantity >> 8) & 0xFF);
  93 + pdu[4] = (byte) (quantity & 0xFF);
  94 + pdu[5] = (byte) byteCount;
  95 + for (int i = 0; i < quantity; i++) {
  96 + pdu[6 + i * 2] = (byte) ((values[i] >> 8) & 0xFF);
  97 + pdu[6 + i * 2 + 1] = (byte) (values[i] & 0xFF);
  98 + }
  99 + return wrapFrame(slaveId, pdu, format, transactionId);
  100 + }
  101 +
  102 + /**
  103 + * 编码写多个线圈请求(FC15)
  104 + * <p>
  105 + * 按 Modbus FC15 标准,线圈值按 bit 打包(每个 byte 包含 8 个线圈状态)。
  106 + *
  107 + * @param slaveId 从站地址
  108 + * @param address 起始地址
  109 + * @param values 线圈值数组(int[],非0 表示 ON,0 表示 OFF)
  110 + * @param format 帧格式
  111 + * @param transactionId 事务 ID(TCP 模式下使用,RTU 模式传 null)
  112 + * @return 编码后的字节数组
  113 + */
  114 + public byte[] encodeWriteMultipleCoilsRequest(int slaveId, int address, int[] values,
  115 + IotModbusFrameFormatEnum format, Integer transactionId) {
  116 + // PDU: [FC(1)] [Address(2)] [Quantity(2)] [ByteCount(1)] [CoilValues(N)]
  117 + int quantity = values.length;
  118 + int byteCount = (quantity + 7) / 8; // 向上取整
  119 + byte[] pdu = new byte[6 + byteCount];
  120 + pdu[0] = (byte) IotModbusCommonUtils.FC_WRITE_MULTIPLE_COILS; // FC15
  121 + pdu[1] = (byte) ((address >> 8) & 0xFF);
  122 + pdu[2] = (byte) (address & 0xFF);
  123 + pdu[3] = (byte) ((quantity >> 8) & 0xFF);
  124 + pdu[4] = (byte) (quantity & 0xFF);
  125 + pdu[5] = (byte) byteCount;
  126 + // 按 bit 打包:每个 byte 的 bit0 对应最低地址的线圈
  127 + for (int i = 0; i < quantity; i++) {
  128 + if (values[i] != 0) {
  129 + pdu[6 + i / 8] |= (byte) (1 << (i % 8));
  130 + }
  131 + }
  132 + return wrapFrame(slaveId, pdu, format, transactionId);
  133 + }
  134 +
  135 + /**
  136 + * 编码自定义功能码帧(认证响应等)
  137 + *
  138 + * @param slaveId 从站地址
  139 + * @param jsonData JSON 数据
  140 + * @param format 帧格式
  141 + * @param transactionId 事务 ID(TCP 模式下使用,RTU 模式传 null)
  142 + * @return 编码后的字节数组
  143 + */
  144 + public byte[] encodeCustomFrame(int slaveId, String jsonData,
  145 + IotModbusFrameFormatEnum format, Integer transactionId) {
  146 + byte[] jsonBytes = jsonData.getBytes(StandardCharsets.UTF_8);
  147 + // PDU: [FC(1)] [ByteCount(1)] [JSON data(N)]
  148 + byte[] pdu = new byte[2 + jsonBytes.length];
  149 + pdu[0] = (byte) customFunctionCode;
  150 + pdu[1] = (byte) jsonBytes.length;
  151 + System.arraycopy(jsonBytes, 0, pdu, 2, jsonBytes.length);
  152 + return wrapFrame(slaveId, pdu, format, transactionId);
  153 + }
  154 +
  155 + // ==================== 帧封装 ====================
  156 +
  157 + /**
  158 + * 将 PDU 封装为完整帧
  159 + *
  160 + * @param slaveId 从站地址
  161 + * @param pdu PDU 数据(含 functionCode)
  162 + * @param format 帧格式
  163 + * @param transactionId 事务 ID(TCP 模式下使用,RTU 模式可为 null)
  164 + * @return 完整帧字节数组
  165 + */
  166 + private byte[] wrapFrame(int slaveId, byte[] pdu, IotModbusFrameFormatEnum format, Integer transactionId) {
  167 + if (format == IotModbusFrameFormatEnum.MODBUS_TCP) {
  168 + return wrapTcpFrame(slaveId, pdu, transactionId != null ? transactionId : 0);
  169 + } else {
  170 + return wrapRtuFrame(slaveId, pdu);
  171 + }
  172 + }
  173 +
  174 + /**
  175 + * 封装 MODBUS_TCP 帧
  176 + * [TransactionId(2)] [ProtocolId(2,=0x0000)] [Length(2)] [UnitId(1)] [PDU...]
  177 + */
  178 + private byte[] wrapTcpFrame(int slaveId, byte[] pdu, int transactionId) {
  179 + int length = 1 + pdu.length; // UnitId + PDU
  180 + byte[] frame = new byte[6 + length]; // MBAP(6) + UnitId(1) + PDU
  181 + // MBAP Header
  182 + frame[0] = (byte) ((transactionId >> 8) & 0xFF);
  183 + frame[1] = (byte) (transactionId & 0xFF);
  184 + frame[2] = 0; // Protocol ID high
  185 + frame[3] = 0; // Protocol ID low
  186 + frame[4] = (byte) ((length >> 8) & 0xFF);
  187 + frame[5] = (byte) (length & 0xFF);
  188 + // Unit ID
  189 + frame[6] = (byte) slaveId;
  190 + // PDU
  191 + System.arraycopy(pdu, 0, frame, 7, pdu.length);
  192 + return frame;
  193 + }
  194 +
  195 + /**
  196 + * 封装 MODBUS_RTU 帧
  197 + * [SlaveId(1)] [PDU...] [CRC(2)]
  198 + */
  199 + private byte[] wrapRtuFrame(int slaveId, byte[] pdu) {
  200 + byte[] frame = new byte[1 + pdu.length + 2]; // SlaveId + PDU + CRC
  201 + frame[0] = (byte) slaveId;
  202 + System.arraycopy(pdu, 0, frame, 1, pdu.length);
  203 + // 计算并追加 CRC16
  204 + int crc = IotModbusCommonUtils.calculateCrc16(frame, frame.length - 2);
  205 + frame[frame.length - 2] = (byte) (crc & 0xFF); // CRC Low
  206 + frame[frame.length - 1] = (byte) ((crc >> 8) & 0xFF); // CRC High
  207 + return frame;
  208 + }
  209 +
  210 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/handler/downstream/IotModbusTcpServerDownstreamHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.handler.downstream;
  2 +
  3 +import cn.hutool.core.lang.Assert;
  4 +import cn.hutool.core.util.ObjUtil;
  5 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
  6 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
  7 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
  8 +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum;
  9 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  10 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
  11 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameEncoder;
  12 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConfigCacheService;
  13 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager;
  14 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager.ConnectionInfo;
  15 +import lombok.extern.slf4j.Slf4j;
  16 +
  17 +import java.util.Map;
  18 +import java.util.concurrent.atomic.AtomicInteger;
  19 +
  20 +/**
  21 + * IoT Modbus TCP Server 下行消息处理器
  22 + * <p>
  23 + * 负责:
  24 + * 1. 处理下行消息(如属性设置 thing.service.property.set)
  25 + * 2. 将属性值转换为 Modbus 写指令,通过 TCP 连接发送给设备
  26 + *
  27 + * @author 芋道源码
  28 + */
  29 +@Slf4j
  30 +public class IotModbusTcpServerDownstreamHandler {
  31 +
  32 + private final IotModbusTcpServerConnectionManager connectionManager;
  33 + private final IotModbusTcpServerConfigCacheService configCacheService;
  34 + private final IotModbusFrameEncoder frameEncoder;
  35 +
  36 + /**
  37 + * TCP 事务 ID 自增器(与 PollScheduler 共享)
  38 + */
  39 + private final AtomicInteger transactionIdCounter;
  40 +
  41 + public IotModbusTcpServerDownstreamHandler(IotModbusTcpServerConnectionManager connectionManager,
  42 + IotModbusTcpServerConfigCacheService configCacheService,
  43 + IotModbusFrameEncoder frameEncoder,
  44 + AtomicInteger transactionIdCounter) {
  45 + this.connectionManager = connectionManager;
  46 + this.configCacheService = configCacheService;
  47 + this.frameEncoder = frameEncoder;
  48 + this.transactionIdCounter = transactionIdCounter;
  49 + }
  50 +
  51 + /**
  52 + * 处理下行消息
  53 + */
  54 + @SuppressWarnings({"unchecked", "DuplicatedCode"})
  55 + public void handle(IotDeviceMessage message) {
  56 + // 1.1 检查是否是属性设置消息
  57 + if (ObjUtil.equals(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), message.getMethod())) {
  58 + return;
  59 + }
  60 + if (ObjUtil.notEqual(IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod(), message.getMethod())) {
  61 + log.debug("[handle][忽略非属性设置消息: {}]", message.getMethod());
  62 + return;
  63 + }
  64 + // 1.2 获取设备配置
  65 + IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(message.getDeviceId());
  66 + if (config == null) {
  67 + log.warn("[handle][设备 {} 没有 Modbus 配置]", message.getDeviceId());
  68 + return;
  69 + }
  70 + // 1.3 获取连接信息
  71 + ConnectionInfo connInfo = connectionManager.getConnectionInfoByDeviceId(message.getDeviceId());
  72 + if (connInfo == null) {
  73 + log.warn("[handle][设备 {} 没有连接]", message.getDeviceId());
  74 + return;
  75 + }
  76 +
  77 + // 2. 解析属性值并写入
  78 + Object params = message.getParams();
  79 + if (!(params instanceof Map)) {
  80 + log.warn("[handle][params 不是 Map 类型: {}]", params);
  81 + return;
  82 + }
  83 + Map<String, Object> propertyMap = (Map<String, Object>) params;
  84 + for (Map.Entry<String, Object> entry : propertyMap.entrySet()) {
  85 + String identifier = entry.getKey();
  86 + Object value = entry.getValue();
  87 + // 2.1 查找对应的点位配置
  88 + IotModbusPointRespDTO point = IotModbusCommonUtils.findPoint(config, identifier);
  89 + if (point == null) {
  90 + log.warn("[handle][设备 {} 没有点位配置: {}]", message.getDeviceId(), identifier);
  91 + continue;
  92 + }
  93 + // 2.2 检查是否支持写操作
  94 + if (!IotModbusCommonUtils.isWritable(point.getFunctionCode())) {
  95 + log.warn("[handle][点位 {} 不支持写操作, 功能码={}]", identifier, point.getFunctionCode());
  96 + continue;
  97 + }
  98 +
  99 + // 2.3 执行写入
  100 + writeProperty(config.getDeviceId(), connInfo, point, value);
  101 + }
  102 + }
  103 +
  104 + /**
  105 + * 写入属性值
  106 + */
  107 + private void writeProperty(Long deviceId, ConnectionInfo connInfo,
  108 + IotModbusPointRespDTO point, Object value) {
  109 + // 1.1 转换属性值为原始值
  110 + int[] rawValues = IotModbusCommonUtils.convertToRawValues(value, point);
  111 +
  112 + // 1.2 确定帧格式和事务 ID
  113 + IotModbusFrameFormatEnum frameFormat = connInfo.getFrameFormat();
  114 + Assert.notNull(frameFormat, "连接帧格式不能为空");
  115 + Integer transactionId = frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP
  116 + ? (transactionIdCounter.incrementAndGet() & 0xFFFF)
  117 + : null;
  118 + int slaveId = connInfo.getSlaveId() != null ? connInfo.getSlaveId() : 1;
  119 + // 1.3 编码写请求
  120 + byte[] data;
  121 + int readFunctionCode = point.getFunctionCode();
  122 + Integer writeSingleCode = IotModbusCommonUtils.getWriteSingleFunctionCode(readFunctionCode);
  123 + Integer writeMultipleCode = IotModbusCommonUtils.getWriteMultipleFunctionCode(readFunctionCode);
  124 + if (rawValues.length == 1 && writeSingleCode != null) {
  125 + // 单个值:使用单写功能码(FC05/FC06)
  126 + data = frameEncoder.encodeWriteSingleRequest(slaveId, writeSingleCode,
  127 + point.getRegisterAddress(), rawValues[0], frameFormat, transactionId);
  128 + } else if (writeMultipleCode != null) {
  129 + // 多个值:使用多写功能码(FC15/FC16)
  130 + if (writeMultipleCode == IotModbusCommonUtils.FC_WRITE_MULTIPLE_COILS) {
  131 + data = frameEncoder.encodeWriteMultipleCoilsRequest(slaveId,
  132 + point.getRegisterAddress(), rawValues, frameFormat, transactionId);
  133 + } else {
  134 + data = frameEncoder.encodeWriteMultipleRegistersRequest(slaveId,
  135 + point.getRegisterAddress(), rawValues, frameFormat, transactionId);
  136 + }
  137 + } else {
  138 + log.warn("[writeProperty][点位 {} 不支持写操作, 功能码={}]", point.getIdentifier(), readFunctionCode);
  139 + return;
  140 + }
  141 +
  142 + // 2. 发送消息
  143 + connectionManager.sendToDevice(deviceId, data).onSuccess(v ->
  144 + log.info("[writeProperty][写入成功, deviceId={}, identifier={}, value={}]",
  145 + deviceId, point.getIdentifier(), value)
  146 + ).onFailure(e ->
  147 + log.error("[writeProperty][写入失败, deviceId={}, identifier={}, value={}]",
  148 + deviceId, point.getIdentifier(), value, e)
  149 + );
  150 + }
  151 +
  152 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/handler/downstream/IotModbusTcpServerDownstreamSubscriber.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.handler.downstream;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
  4 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber;
  6 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.IotModbusTcpServerProtocol;
  7 +import lombok.extern.slf4j.Slf4j;
  8 +
  9 +/**
  10 + * IoT Modbus TCP Server 下行消息订阅器:订阅消息总线的下行消息并转发给处理器
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +@Slf4j
  15 +public class IotModbusTcpServerDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
  16 +
  17 + private final IotModbusTcpServerDownstreamHandler downstreamHandler;
  18 +
  19 + public IotModbusTcpServerDownstreamSubscriber(IotModbusTcpServerProtocol protocol,
  20 + IotModbusTcpServerDownstreamHandler downstreamHandler,
  21 + IotMessageBus messageBus) {
  22 + super(protocol, messageBus);
  23 + this.downstreamHandler = downstreamHandler;
  24 + }
  25 +
  26 + @Override
  27 + protected void handleMessage(IotDeviceMessage message) {
  28 + downstreamHandler.handle(message);
  29 + }
  30 +
  31 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/handler/upstream/IotModbusTcpServerUpstreamHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.handler.upstream;
  2 +
  3 +import cn.hutool.core.lang.Assert;
  4 +import cn.hutool.core.map.MapUtil;
  5 +import cn.hutool.core.util.BooleanUtil;
  6 +import cn.hutool.core.util.ObjUtil;
  7 +import cn.hutool.core.util.StrUtil;
  8 +import cn.iocoder.yudao.framework.common.exception.ServiceException;
  9 +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
  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;
  13 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
  14 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
  15 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
  16 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
  17 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
  18 +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum;
  19 +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.util.IotDeviceAuthUtils;
  22 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
  23 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrame;
  24 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameEncoder;
  25 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConfigCacheService;
  26 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager;
  27 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager.ConnectionInfo;
  28 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerPendingRequestManager;
  29 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerPendingRequestManager.PendingRequest;
  30 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerPollScheduler;
  31 +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
  32 +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  33 +import io.vertx.core.net.NetSocket;
  34 +import lombok.extern.slf4j.Slf4j;
  35 +
  36 +import java.util.Map;
  37 +
  38 +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
  39 +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
  40 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
  41 +
  42 +/**
  43 + * IoT Modbus TCP Server 上行数据处理器
  44 + * <p>
  45 + * 处理:
  46 + * 1. 自定义 FC 认证
  47 + * 2. 轮询响应 → 点位翻译 → thing.property.post
  48 + *
  49 + * @author 芋道源码
  50 + */
  51 +@Slf4j
  52 +public class IotModbusTcpServerUpstreamHandler {
  53 +
  54 + private static final String METHOD_AUTH = "auth";
  55 +
  56 + private final IotDeviceCommonApi deviceApi;
  57 + private final IotDeviceMessageService messageService;
  58 + private final IotModbusFrameEncoder frameEncoder;
  59 + private final IotModbusTcpServerConnectionManager connectionManager;
  60 + private final IotModbusTcpServerConfigCacheService configCacheService;
  61 + private final IotModbusTcpServerPendingRequestManager pendingRequestManager;
  62 + private final IotModbusTcpServerPollScheduler pollScheduler;
  63 + private final IotDeviceService deviceService;
  64 +
  65 + private final String serverId;
  66 +
  67 + public IotModbusTcpServerUpstreamHandler(IotDeviceCommonApi deviceApi,
  68 + IotDeviceMessageService messageService,
  69 + IotModbusFrameEncoder frameEncoder,
  70 + IotModbusTcpServerConnectionManager connectionManager,
  71 + IotModbusTcpServerConfigCacheService configCacheService,
  72 + IotModbusTcpServerPendingRequestManager pendingRequestManager,
  73 + IotModbusTcpServerPollScheduler pollScheduler,
  74 + IotDeviceService deviceService,
  75 + String serverId) {
  76 + this.deviceApi = deviceApi;
  77 + this.messageService = messageService;
  78 + this.frameEncoder = frameEncoder;
  79 + this.connectionManager = connectionManager;
  80 + this.configCacheService = configCacheService;
  81 + this.pendingRequestManager = pendingRequestManager;
  82 + this.pollScheduler = pollScheduler;
  83 + this.deviceService = deviceService;
  84 + this.serverId = serverId;
  85 + }
  86 +
  87 + // ========== 帧处理入口 ==========
  88 +
  89 + /**
  90 + * 处理帧
  91 + */
  92 + public void handleFrame(NetSocket socket, IotModbusFrame frame, IotModbusFrameFormatEnum frameFormat) {
  93 + if (frame == null) {
  94 + return;
  95 + }
  96 + // 1. 异常响应
  97 + if (frame.isException()) {
  98 + log.warn("[handleFrame][设备异常响应, slaveId={}, FC={}, exceptionCode={}]",
  99 + frame.getSlaveId(), frame.getFunctionCode(), frame.getExceptionCode());
  100 + return;
  101 + }
  102 +
  103 + // 2. 情况一:自定义功能码(认证等扩展)
  104 + if (StrUtil.isNotEmpty(frame.getCustomData())) {
  105 + handleCustomFrame(socket, frame, frameFormat);
  106 + return;
  107 + }
  108 +
  109 + // 3. 情况二:标准 Modbus 响应 → 轮询响应处理
  110 + handlePollingResponse(socket, frame, frameFormat);
  111 + }
  112 +
  113 + // ========== 自定义 FC 处理(认证等) ==========
  114 +
  115 + /**
  116 + * 处理自定义功能码帧
  117 + * <p>
  118 + * 异常分层翻译,参考 {@link cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpAbstractHandler}
  119 + */
  120 + private void handleCustomFrame(NetSocket socket, IotModbusFrame frame, IotModbusFrameFormatEnum frameFormat) {
  121 + String method = null;
  122 + try {
  123 + IotDeviceMessage message = JsonUtils.parseObject(frame.getCustomData(), IotDeviceMessage.class);
  124 + if (message == null) {
  125 + throw invalidParamException("自定义 FC 数据解析失败");
  126 + }
  127 + method = message.getMethod();
  128 + if (METHOD_AUTH.equals(method)) {
  129 + handleAuth(socket, frame, frameFormat, message.getParams());
  130 + return;
  131 + }
  132 + log.warn("[handleCustomFrame][未知 method: {}, frame: slaveId={}, FC={}, customData={}]",
  133 + method, frame.getSlaveId(), frame.getFunctionCode(), frame.getCustomData());
  134 + } catch (ServiceException e) {
  135 + // 已知业务异常,返回对应的错误码和错误信息
  136 + sendCustomResponse(socket, frame, frameFormat, method, e.getCode(), e.getMessage());
  137 + } catch (IllegalArgumentException e) {
  138 + // 参数校验异常,返回 400 错误
  139 + sendCustomResponse(socket, frame, frameFormat, method, BAD_REQUEST.getCode(), e.getMessage());
  140 + } catch (Exception e) {
  141 + // 其他未知异常,返回 500 错误
  142 + log.error("[handleCustomFrame][解析自定义 FC 数据失败, frame: slaveId={}, FC={}, customData={}]",
  143 + frame.getSlaveId(), frame.getFunctionCode(), frame.getCustomData(), e);
  144 + sendCustomResponse(socket, frame, frameFormat, method,
  145 + INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
  146 + }
  147 + }
  148 +
  149 + /**
  150 + * 处理认证请求
  151 + */
  152 + @SuppressWarnings("DataFlowIssue")
  153 + private void handleAuth(NetSocket socket, IotModbusFrame frame, IotModbusFrameFormatEnum frameFormat, Object params) {
  154 + // 1. 解析认证参数
  155 + IotDeviceAuthReqDTO request = JsonUtils.convertObject(params, IotDeviceAuthReqDTO.class);
  156 + Assert.notNull(request, "认证参数不能为空");
  157 + Assert.notBlank(request.getUsername(), "username 不能为空");
  158 + Assert.notBlank(request.getPassword(), "password 不能为空");
  159 + // 特殊:考虑到 modbus 消息体积较小,默认 clientId 传递空串
  160 + if (StrUtil.isBlank(request.getClientId())) {
  161 + request.setClientId(IotDeviceAuthUtils.buildClientIdFromUsername(request.getUsername()));
  162 + }
  163 + Assert.notBlank(request.getClientId(), "clientId 不能为空");
  164 +
  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, BAD_REQUEST.getCode(), "认证失败");
  171 + return;
  172 + }
  173 + // 2.2 解析设备信息
  174 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(request.getUsername());
  175 + Assert.notNull(deviceInfo, "解析设备信息失败");
  176 + // 2.3 获取设备信息
  177 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), deviceInfo.getDeviceName());
  178 + Assert.notNull(device, "设备不存在");
  179 + // 2.4 加载设备 Modbus 配置,无配置则阻断认证
  180 + IotModbusDeviceConfigRespDTO modbusConfig = configCacheService.loadDeviceConfig(device.getId());
  181 + if (modbusConfig == null) {
  182 + log.warn("[handleAuth][设备 {} 没有 Modbus 点位配置, 拒绝认证]", device.getId());
  183 + sendCustomResponse(socket, frame, frameFormat, METHOD_AUTH, BAD_REQUEST.getCode(), "设备无 Modbus 配置");
  184 + return;
  185 + }
  186 + // 2.5 协议不一致,阻断认证
  187 + if (ObjUtil.notEqual(frameFormat.getFormat(), modbusConfig.getFrameFormat())) {
  188 + log.warn("[handleAuth][设备 {} frameFormat 不一致, 连接协议={}, 设备配置={},拒绝认证]",
  189 + device.getId(), frameFormat.getFormat(), modbusConfig.getFrameFormat());
  190 + sendCustomResponse(socket, frame, frameFormat, METHOD_AUTH, BAD_REQUEST.getCode(),
  191 + "frameFormat 协议不一致");
  192 + return;
  193 + }
  194 +
  195 + // 3.1 注册连接
  196 + ConnectionInfo connectionInfo = new ConnectionInfo()
  197 + .setDeviceId(device.getId())
  198 + .setProductKey(deviceInfo.getProductKey())
  199 + .setDeviceName(deviceInfo.getDeviceName())
  200 + .setSlaveId(frame.getSlaveId())
  201 + .setFrameFormat(frameFormat);
  202 + connectionManager.registerConnection(socket, connectionInfo);
  203 + // 3.2 发送上线消息
  204 + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline();
  205 + messageService.sendDeviceMessage(onlineMessage, deviceInfo.getProductKey(), deviceInfo.getDeviceName(), serverId);
  206 + // 3.3 发送成功响应
  207 + sendCustomResponse(socket, frame, frameFormat, METHOD_AUTH,
  208 + GlobalErrorCodeConstants.SUCCESS.getCode(), "success");
  209 + log.info("[handleAuth][认证成功, clientId={}, deviceId={}]", request.getClientId(), device.getId());
  210 +
  211 + // 4. 启动轮询
  212 + pollScheduler.updatePolling(modbusConfig);
  213 + }
  214 +
  215 + /**
  216 + * 发送自定义功能码响应
  217 + */
  218 + private void sendCustomResponse(NetSocket socket, IotModbusFrame frame,
  219 + IotModbusFrameFormatEnum frameFormat,
  220 + String method, int code, String message) {
  221 + Map<String, Object> response = MapUtil.<String, Object>builder()
  222 + .put("method", method)
  223 + .put("code", code)
  224 + .put("message", message)
  225 + .build();
  226 + byte[] data = frameEncoder.encodeCustomFrame(frame.getSlaveId(), JsonUtils.toJsonString(response),
  227 + frameFormat, frame.getTransactionId());
  228 + connectionManager.sendToSocket(socket, data);
  229 + }
  230 +
  231 + // ========== 轮询响应处理 ==========
  232 +
  233 + /**
  234 + * 处理轮询响应(云端轮询模式)
  235 + */
  236 + private void handlePollingResponse(NetSocket socket, IotModbusFrame frame,
  237 + IotModbusFrameFormatEnum frameFormat) {
  238 + // 1. 获取连接信息(未认证连接丢弃)
  239 + ConnectionInfo info = connectionManager.getConnectionInfo(socket);
  240 + if (info == null) {
  241 + log.warn("[handlePollingResponse][未认证连接, 丢弃数据, remoteAddress={}]", socket.remoteAddress());
  242 + return;
  243 + }
  244 +
  245 + // 2.1 匹配 PendingRequest
  246 + PendingRequest request = pendingRequestManager.matchResponse(
  247 + info.getDeviceId(), frame, frameFormat);
  248 + if (request == null) {
  249 + log.debug("[handlePollingResponse][未匹配到 PendingRequest, deviceId={}, FC={}]",
  250 + info.getDeviceId(), frame.getFunctionCode());
  251 + return;
  252 + }
  253 + // 2.2 提取寄存器值
  254 + int[] rawValues = IotModbusCommonUtils.extractValues(frame);
  255 + if (rawValues == null) {
  256 + log.warn("[handlePollingResponse][提取寄存器值失败, deviceId={}, identifier={}]",
  257 + info.getDeviceId(), request.getIdentifier());
  258 + return;
  259 + }
  260 + // 2.3 查找点位配置
  261 + IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(info.getDeviceId());
  262 + IotModbusPointRespDTO point = IotModbusCommonUtils.findPointById(config, request.getPointId());
  263 + if (point == null) {
  264 + return;
  265 + }
  266 +
  267 + // 3.1 转换原始值为物模型属性值(点位翻译)
  268 + Object convertedValue = IotModbusCommonUtils.convertToPropertyValue(rawValues, point);
  269 + // 3.2 构造属性上报消息
  270 + Map<String, Object> params = MapUtil.of(request.getIdentifier(), convertedValue);
  271 + IotDeviceMessage message = IotDeviceMessage.requestOf(
  272 + IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), params);
  273 +
  274 + // 4. 发送到消息总线
  275 + messageService.sendDeviceMessage(message, info.getProductKey(), info.getDeviceName(), serverId);
  276 + log.debug("[handlePollingResponse][设备={}, 属性={}, 原始值={}, 转换值={}]",
  277 + info.getDeviceId(), request.getIdentifier(), rawValues, convertedValue);
  278 + }
  279 +
  280 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerConfigCacheService.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  5 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  6 +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
  7 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO;
  8 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
  9 +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusModeEnum;
  10 +import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
  11 +import lombok.RequiredArgsConstructor;
  12 +import lombok.extern.slf4j.Slf4j;
  13 +
  14 +import java.util.Collections;
  15 +import java.util.List;
  16 +import java.util.Map;
  17 +import java.util.Set;
  18 +import java.util.concurrent.ConcurrentHashMap;
  19 +
  20 +/**
  21 + * IoT Modbus TCP Server 配置缓存:认证时按需加载,断连时清理,定时刷新已连接设备
  22 + *
  23 + * @author 芋道源码
  24 + */
  25 +@RequiredArgsConstructor
  26 +@Slf4j
  27 +public class IotModbusTcpServerConfigCacheService {
  28 +
  29 + private final IotDeviceCommonApi deviceApi;
  30 +
  31 + /**
  32 + * 配置缓存:deviceId -> 配置
  33 + */
  34 + private final Map<Long, IotModbusDeviceConfigRespDTO> configCache = new ConcurrentHashMap<>();
  35 +
  36 + /**
  37 + * 加载单个设备的配置(认证成功后调用)
  38 + *
  39 + * @param deviceId 设备 ID
  40 + * @return 设备配置
  41 + */
  42 + public IotModbusDeviceConfigRespDTO loadDeviceConfig(Long deviceId) {
  43 + try {
  44 + // 1. 从远程 API 获取配置
  45 + IotModbusDeviceConfigListReqDTO reqDTO = new IotModbusDeviceConfigListReqDTO()
  46 + .setStatus(CommonStatusEnum.ENABLE.getStatus())
  47 + .setMode(IotModbusModeEnum.POLLING.getMode())
  48 + .setProtocolType(IotProtocolTypeEnum.MODBUS_TCP_SERVER.getType())
  49 + .setDeviceIds(Collections.singleton(deviceId));
  50 + CommonResult<List<IotModbusDeviceConfigRespDTO>> result = deviceApi.getModbusDeviceConfigList(reqDTO);
  51 + result.checkError();
  52 + IotModbusDeviceConfigRespDTO modbusConfig = CollUtil.getFirst(result.getData());
  53 + if (modbusConfig == null) {
  54 + log.warn("[loadDeviceConfig][远程获取配置失败,未找到数据, deviceId={}]", deviceId);
  55 + return null;
  56 + }
  57 +
  58 + // 2. 更新缓存并返回
  59 + configCache.put(modbusConfig.getDeviceId(), modbusConfig);
  60 + return modbusConfig;
  61 + } catch (Exception e) {
  62 + log.error("[loadDeviceConfig][从远程获取配置失败, deviceId={}]", deviceId, e);
  63 + return null;
  64 + }
  65 + }
  66 +
  67 + /**
  68 + * 刷新已连接设备的配置缓存
  69 + * <p>
  70 + * 定时调用,从远程 API 拉取最新配置,只更新已连接设备的缓存。
  71 + *
  72 + * @param connectedDeviceIds 当前已连接的设备 ID 集合
  73 + * @return 已连接设备的最新配置列表
  74 + */
  75 + public List<IotModbusDeviceConfigRespDTO> refreshConnectedDeviceConfigList(Set<Long> connectedDeviceIds) {
  76 + if (CollUtil.isEmpty(connectedDeviceIds)) {
  77 + return Collections.emptyList();
  78 + }
  79 + try {
  80 + // 1. 从远程获取已连接设备的配置
  81 + CommonResult<List<IotModbusDeviceConfigRespDTO>> result = deviceApi.getModbusDeviceConfigList(
  82 + new IotModbusDeviceConfigListReqDTO().setStatus(CommonStatusEnum.ENABLE.getStatus())
  83 + .setMode(IotModbusModeEnum.POLLING.getMode())
  84 + .setProtocolType(IotProtocolTypeEnum.MODBUS_TCP_SERVER.getType())
  85 + .setDeviceIds(connectedDeviceIds));
  86 + List<IotModbusDeviceConfigRespDTO> modbusConfigs = result.getCheckedData();
  87 +
  88 + // 2. 更新缓存并返回
  89 + for (IotModbusDeviceConfigRespDTO config : modbusConfigs) {
  90 + configCache.put(config.getDeviceId(), config);
  91 + }
  92 + return modbusConfigs;
  93 + } catch (Exception e) {
  94 + log.error("[refreshConnectedDeviceConfigList][刷新配置失败]", e);
  95 + return null;
  96 + }
  97 + }
  98 +
  99 + /**
  100 + * 获取设备配置
  101 + */
  102 + public IotModbusDeviceConfigRespDTO getConfig(Long deviceId) {
  103 + IotModbusDeviceConfigRespDTO config = configCache.get(deviceId);
  104 + if (config != null) {
  105 + return config;
  106 + }
  107 + // 缓存未命中,从远程 API 获取
  108 + return loadDeviceConfig(deviceId);
  109 + }
  110 +
  111 + /**
  112 + * 移除设备配置缓存(设备断连时调用)
  113 + */
  114 + public void removeConfig(Long deviceId) {
  115 + configCache.remove(deviceId);
  116 + }
  117 +
  118 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerConnectionManager.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum;
  4 +import io.vertx.core.Future;
  5 +import io.vertx.core.buffer.Buffer;
  6 +import io.vertx.core.net.NetSocket;
  7 +import lombok.Data;
  8 +import lombok.experimental.Accessors;
  9 +import lombok.extern.slf4j.Slf4j;
  10 +
  11 +import java.util.ArrayList;
  12 +import java.util.List;
  13 +import java.util.Map;
  14 +import java.util.Set;
  15 +import java.util.concurrent.ConcurrentHashMap;
  16 +
  17 +/**
  18 + * IoT Modbus TCP Server 连接管理器
  19 + * <p>
  20 + * 管理设备 TCP 连接:socket ↔ 设备双向映射
  21 + *
  22 + * @author 芋道源码
  23 + */
  24 +@Slf4j
  25 +public class IotModbusTcpServerConnectionManager {
  26 +
  27 + /**
  28 + * socket → 连接信息
  29 + */
  30 + private final Map<NetSocket, ConnectionInfo> connectionMap = new ConcurrentHashMap<>();
  31 +
  32 + /**
  33 + * deviceId → socket
  34 + */
  35 + private final Map<Long, NetSocket> deviceSocketMap = new ConcurrentHashMap<>();
  36 +
  37 + /**
  38 + * 连接信息
  39 + */
  40 + @Data
  41 + @Accessors(chain = true)
  42 + public static class ConnectionInfo {
  43 +
  44 + /**
  45 + * 设备编号
  46 + */
  47 + private Long deviceId;
  48 + /**
  49 + * 产品标识
  50 + */
  51 + private String productKey;
  52 + /**
  53 + * 设备名称
  54 + */
  55 + private String deviceName;
  56 + /**
  57 + * 从站地址
  58 + */
  59 + private Integer slaveId;
  60 +
  61 + /**
  62 + * 帧格式(首帧自动检测得到)
  63 + */
  64 + private IotModbusFrameFormatEnum frameFormat;
  65 +
  66 + }
  67 +
  68 + /**
  69 + * 注册已认证的连接
  70 + */
  71 + public void registerConnection(NetSocket socket, ConnectionInfo info) {
  72 + // 先检查该设备是否有旧连接,若有且不是同一个 socket,关闭旧 socket
  73 + NetSocket oldSocket = deviceSocketMap.get(info.getDeviceId());
  74 + if (oldSocket != null && oldSocket != socket) {
  75 + log.info("[registerConnection][设备 {} 存在旧连接, 关闭旧 socket, oldRemote={}, newRemote={}]",
  76 + info.getDeviceId(), oldSocket.remoteAddress(), socket.remoteAddress());
  77 + connectionMap.remove(oldSocket);
  78 + try {
  79 + oldSocket.close();
  80 + } catch (Exception e) {
  81 + log.warn("[registerConnection][关闭旧 socket 失败, deviceId={}, oldRemote={}]",
  82 + info.getDeviceId(), oldSocket.remoteAddress(), e);
  83 + }
  84 + }
  85 +
  86 + // 注册新连接
  87 + connectionMap.put(socket, info);
  88 + deviceSocketMap.put(info.getDeviceId(), socket);
  89 + log.info("[registerConnection][设备 {} 连接已注册, remoteAddress={}]",
  90 + info.getDeviceId(), socket.remoteAddress());
  91 + }
  92 +
  93 + /**
  94 + * 获取连接信息
  95 + */
  96 + public ConnectionInfo getConnectionInfo(NetSocket socket) {
  97 + return connectionMap.get(socket);
  98 + }
  99 +
  100 + /**
  101 + * 根据设备 ID 获取连接信息
  102 + */
  103 + public ConnectionInfo getConnectionInfoByDeviceId(Long deviceId) {
  104 + NetSocket socket = deviceSocketMap.get(deviceId);
  105 + return socket != null ? connectionMap.get(socket) : null;
  106 + }
  107 +
  108 + /**
  109 + * 获取所有已连接设备的 ID 集合
  110 + */
  111 + public Set<Long> getConnectedDeviceIds() {
  112 + return deviceSocketMap.keySet();
  113 + }
  114 +
  115 + /**
  116 + * 移除连接
  117 + */
  118 + public ConnectionInfo removeConnection(NetSocket socket) {
  119 + ConnectionInfo info = connectionMap.remove(socket);
  120 + if (info != null && info.getDeviceId() != null) {
  121 + // 使用两参数 remove:只有当 deviceSocketMap 中对应的 socket 就是当前 socket 时才删除,
  122 + // 避免新 socket 已注册后旧 socket 关闭时误删新映射
  123 + boolean removed = deviceSocketMap.remove(info.getDeviceId(), socket);
  124 + if (removed) {
  125 + log.info("[removeConnection][设备 {} 连接已移除]", info.getDeviceId());
  126 + } else {
  127 + log.info("[removeConnection][设备 {} 旧连接关闭, 新连接仍在线, 跳过清理]", info.getDeviceId());
  128 + }
  129 + }
  130 + return info;
  131 + }
  132 +
  133 + /**
  134 + * 发送数据到设备
  135 + *
  136 + * @return 发送结果 Future
  137 + */
  138 + public Future<Void> sendToDevice(Long deviceId, byte[] data) {
  139 + NetSocket socket = deviceSocketMap.get(deviceId);
  140 + if (socket == null) {
  141 + log.warn("[sendToDevice][设备 {} 没有连接]", deviceId);
  142 + return Future.failedFuture("设备 " + deviceId + " 没有连接");
  143 + }
  144 + return sendToSocket(socket, data);
  145 + }
  146 +
  147 + /**
  148 + * 发送数据到指定 socket
  149 + *
  150 + * @return 发送结果 Future
  151 + */
  152 + public Future<Void> sendToSocket(NetSocket socket, byte[] data) {
  153 + return socket.write(Buffer.buffer(data));
  154 + }
  155 +
  156 + /**
  157 + * 关闭所有连接
  158 + */
  159 + public void closeAll() {
  160 + // 1. 先复制再清空,避免 closeHandler 回调时并发修改
  161 + List<NetSocket> sockets = new ArrayList<>(connectionMap.keySet());
  162 + connectionMap.clear();
  163 + deviceSocketMap.clear();
  164 + // 2. 关闭所有 socket(closeHandler 中 removeConnection 发现 map 为空会安全跳过)
  165 + for (NetSocket socket : sockets) {
  166 + try {
  167 + socket.close();
  168 + } catch (Exception e) {
  169 + log.error("[closeAll][关闭连接失败]", e);
  170 + }
  171 + }
  172 + }
  173 +
  174 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerPendingRequestManager.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
  6 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrame;
  7 +import lombok.AllArgsConstructor;
  8 +import lombok.Data;
  9 +import lombok.extern.slf4j.Slf4j;
  10 +
  11 +import java.util.Deque;
  12 +import java.util.Iterator;
  13 +import java.util.Map;
  14 +import java.util.concurrent.ConcurrentHashMap;
  15 +import java.util.concurrent.ConcurrentLinkedDeque;
  16 +
  17 +/**
  18 + * IoT Modbus TCP Server 待响应请求管理器
  19 + * <p>
  20 + * 管理轮询下发的请求,用于匹配设备响应:
  21 + * - TCP 模式:按 transactionId 精确匹配
  22 + * - RTU 模式:按 slaveId + functionCode FIFO 匹配
  23 + *
  24 + * @author 芋道源码
  25 + */
  26 +@Slf4j
  27 +public class IotModbusTcpServerPendingRequestManager {
  28 +
  29 + /**
  30 + * deviceId → 有序队列
  31 + */
  32 + private final Map<Long, Deque<PendingRequest>> pendingRequests = new ConcurrentHashMap<>();
  33 +
  34 + /**
  35 + * 待响应请求信息
  36 + */
  37 + @Data
  38 + @AllArgsConstructor
  39 + public static class PendingRequest {
  40 +
  41 + private Long deviceId;
  42 + private Long pointId;
  43 + private String identifier;
  44 + private int slaveId;
  45 + private int functionCode;
  46 + private int registerAddress;
  47 + private int registerCount;
  48 + private Integer transactionId;
  49 + private long expireAt;
  50 +
  51 + }
  52 +
  53 + /**
  54 + * 添加待响应请求
  55 + */
  56 + public void addRequest(PendingRequest request) {
  57 + pendingRequests.computeIfAbsent(request.getDeviceId(), k -> new ConcurrentLinkedDeque<>())
  58 + .addLast(request);
  59 + }
  60 +
  61 + /**
  62 + * 匹配响应(TCP 模式按 transactionId,RTU 模式按 FIFO)
  63 + *
  64 + * @param deviceId 设备 ID
  65 + * @param frame 收到的响应帧
  66 + * @param frameFormat 帧格式
  67 + * @return 匹配到的 PendingRequest,没有匹配返回 null
  68 + */
  69 + public PendingRequest matchResponse(Long deviceId, IotModbusFrame frame,
  70 + IotModbusFrameFormatEnum frameFormat) {
  71 + Deque<PendingRequest> queue = pendingRequests.get(deviceId);
  72 + if (CollUtil.isEmpty(queue)) {
  73 + return null;
  74 + }
  75 +
  76 + // TCP 模式:按 transactionId 精确匹配
  77 + if (frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP && frame.getTransactionId() != null) {
  78 + return matchByTransactionId(queue, frame.getTransactionId());
  79 + }
  80 + // RTU 模式:FIFO,匹配 slaveId + functionCode + registerCount
  81 + int responseRegisterCount = IotModbusCommonUtils.extractRegisterCountFromResponse(frame);
  82 + return matchByFifo(queue, frame.getSlaveId(), frame.getFunctionCode(), responseRegisterCount);
  83 + }
  84 +
  85 + /**
  86 + * 按 transactionId 匹配
  87 + */
  88 + private PendingRequest matchByTransactionId(Deque<PendingRequest> queue, int transactionId) {
  89 + Iterator<PendingRequest> it = queue.iterator();
  90 + while (it.hasNext()) {
  91 + PendingRequest req = it.next();
  92 + if (req.getTransactionId() != null && req.getTransactionId() == transactionId) {
  93 + it.remove();
  94 + return req;
  95 + }
  96 + }
  97 + return null;
  98 + }
  99 +
  100 + /**
  101 + * 按 FIFO 匹配(slaveId + functionCode + registerCount)
  102 + */
  103 + private PendingRequest matchByFifo(Deque<PendingRequest> queue, int slaveId, int functionCode,
  104 + int responseRegisterCount) {
  105 + Iterator<PendingRequest> it = queue.iterator();
  106 + while (it.hasNext()) {
  107 + PendingRequest req = it.next();
  108 + if (req.getSlaveId() == slaveId
  109 + && req.getFunctionCode() == functionCode
  110 + && (responseRegisterCount <= 0 || req.getRegisterCount() == responseRegisterCount)) {
  111 + it.remove();
  112 + return req;
  113 + }
  114 + }
  115 + return null;
  116 + }
  117 +
  118 + /**
  119 + * 清理过期请求
  120 + */
  121 + public void cleanupExpired() {
  122 + long now = System.currentTimeMillis();
  123 + for (Map.Entry<Long, Deque<PendingRequest>> entry : pendingRequests.entrySet()) {
  124 + Deque<PendingRequest> queue = entry.getValue();
  125 + int removed = 0;
  126 + Iterator<PendingRequest> it = queue.iterator();
  127 + while (it.hasNext()) {
  128 + PendingRequest req = it.next();
  129 + if (req.getExpireAt() < now) {
  130 + it.remove();
  131 + removed++;
  132 + }
  133 + }
  134 + if (removed > 0) {
  135 + log.debug("[cleanupExpired][设备 {} 清理了 {} 个过期请求]", entry.getKey(), removed);
  136 + }
  137 + }
  138 + }
  139 +
  140 + /**
  141 + * 清理指定设备的所有待响应请求
  142 + */
  143 + public void removeDevice(Long deviceId) {
  144 + pendingRequests.remove(deviceId);
  145 + }
  146 +
  147 + /**
  148 + * 清理所有待响应请求
  149 + */
  150 + public void clear() {
  151 + pendingRequests.clear();
  152 + }
  153 +
  154 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/manager/IotModbusTcpServerPollScheduler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.lang.Assert;
  5 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
  6 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
  7 +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum;
  8 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.manager.AbstractIotModbusPollScheduler;
  9 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
  10 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameEncoder;
  11 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerConnectionManager.ConnectionInfo;
  12 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.manager.IotModbusTcpServerPendingRequestManager.PendingRequest;
  13 +import io.vertx.core.Vertx;
  14 +import lombok.Getter;
  15 +import lombok.extern.slf4j.Slf4j;
  16 +
  17 +import java.util.concurrent.atomic.AtomicInteger;
  18 +
  19 +/**
  20 + * IoT Modbus TCP Server 轮询调度器:编码读请求帧,通过 TCP 连接发送到设备,注册 PendingRequest 等待响应
  21 + *
  22 + * @author 芋道源码
  23 + */
  24 +@Slf4j
  25 +public class IotModbusTcpServerPollScheduler extends AbstractIotModbusPollScheduler {
  26 +
  27 + private final IotModbusTcpServerConnectionManager connectionManager;
  28 + private final IotModbusFrameEncoder frameEncoder;
  29 + private final IotModbusTcpServerPendingRequestManager pendingRequestManager;
  30 + private final IotModbusTcpServerConfigCacheService configCacheService;
  31 + private final int requestTimeout;
  32 + /**
  33 + * TCP 事务 ID 自增器(与 DownstreamHandler 共享)
  34 + */
  35 + @Getter
  36 + private final AtomicInteger transactionIdCounter;
  37 +
  38 + public IotModbusTcpServerPollScheduler(Vertx vertx,
  39 + IotModbusTcpServerConnectionManager connectionManager,
  40 + IotModbusFrameEncoder frameEncoder,
  41 + IotModbusTcpServerPendingRequestManager pendingRequestManager,
  42 + int requestTimeout,
  43 + AtomicInteger transactionIdCounter,
  44 + IotModbusTcpServerConfigCacheService configCacheService) {
  45 + super(vertx);
  46 + this.connectionManager = connectionManager;
  47 + this.frameEncoder = frameEncoder;
  48 + this.pendingRequestManager = pendingRequestManager;
  49 + this.requestTimeout = requestTimeout;
  50 + this.transactionIdCounter = transactionIdCounter;
  51 + this.configCacheService = configCacheService;
  52 + }
  53 +
  54 + // ========== 轮询执行 ==========
  55 +
  56 + /**
  57 + * 轮询单个点位
  58 + */
  59 + @Override
  60 + @SuppressWarnings("DuplicatedCode")
  61 + protected void pollPoint(Long deviceId, Long pointId) {
  62 + // 1.1 从 configCache 获取最新配置
  63 + IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(deviceId);
  64 + if (config == null || CollUtil.isEmpty(config.getPoints())) {
  65 + log.warn("[pollPoint][设备 {} 没有配置]", deviceId);
  66 + return;
  67 + }
  68 + // 1.2 查找点位
  69 + IotModbusPointRespDTO point = IotModbusCommonUtils.findPointById(config, pointId);
  70 + if (point == null) {
  71 + log.warn("[pollPoint][设备 {} 点位 {} 未找到]", deviceId, pointId);
  72 + return;
  73 + }
  74 +
  75 + // 2.1 获取连接
  76 + ConnectionInfo connection = connectionManager.getConnectionInfoByDeviceId(deviceId);
  77 + if (connection == null) {
  78 + log.debug("[pollPoint][设备 {} 没有连接,跳过轮询]", deviceId);
  79 + return;
  80 + }
  81 + // 2.2 获取 slave ID
  82 + IotModbusFrameFormatEnum frameFormat = connection.getFrameFormat();
  83 + Assert.notNull(frameFormat, "设备 {} 的帧格式不能为空", deviceId);
  84 + Integer slaveId = connection.getSlaveId();
  85 + Assert.notNull(connection.getSlaveId(), "设备 {} 的 slaveId 不能为空", deviceId);
  86 +
  87 + // 3.1 编码读请求
  88 + Integer transactionId = frameFormat == IotModbusFrameFormatEnum.MODBUS_TCP
  89 + ? (transactionIdCounter.incrementAndGet() & 0xFFFF)
  90 + : null;
  91 + byte[] data = frameEncoder.encodeReadRequest(slaveId, point.getFunctionCode(),
  92 + point.getRegisterAddress(), point.getRegisterCount(), frameFormat, transactionId);
  93 + // 3.2 注册 PendingRequest
  94 + PendingRequest pendingRequest = new PendingRequest(
  95 + deviceId, point.getId(), point.getIdentifier(),
  96 + slaveId, point.getFunctionCode(),
  97 + point.getRegisterAddress(), point.getRegisterCount(),
  98 + transactionId,
  99 + System.currentTimeMillis() + requestTimeout);
  100 + pendingRequestManager.addRequest(pendingRequest);
  101 + // 3.3 发送读请求
  102 + connectionManager.sendToDevice(deviceId, data).onSuccess(v ->
  103 + log.debug("[pollPoint][设备={}, 点位={}, FC={}, 地址={}, 数量={}]",
  104 + deviceId, point.getIdentifier(), point.getFunctionCode(),
  105 + point.getRegisterAddress(), point.getRegisterCount())
  106 + ).onFailure(e ->
  107 + log.warn("[pollPoint][发送失败, 设备={}, 点位={}]", deviceId, point.getIdentifier(), e)
  108 + );
  109 + }
  110 +
  111 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/package-info.java 0 → 100644
  1 +/**
  2 + * Modbus TCP Server(从站)协议:设备主动连接网关,自定义 FC65 认证后由网关云端轮询
  3 + * <p>
  4 + * TCP Server 模式,支持 MODBUS_TCP / MODBUS_RTU 帧格式自动检测
  5 + */
  6 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver;
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java
... ... @@ -81,7 +81,7 @@ public class IotMqttProtocol implements IotProtocol {
81 81 /**
82 82 * 下行消息订阅者
83 83 */
84   - private final IotMqttDownstreamSubscriber downstreamSubscriber;
  84 + private IotMqttDownstreamSubscriber downstreamSubscriber;
85 85  
86 86 private final IotDeviceMessageService deviceMessageService;
87 87  
... ... @@ -104,11 +104,6 @@ public class IotMqttProtocol implements IotProtocol {
104 104 this.authHandler = new IotMqttAuthHandler(connectionManager, deviceMessageService, deviceApi, serverId);
105 105 this.registerHandler = new IotMqttRegisterHandler(connectionManager, deviceMessageService);
106 106 this.upstreamHandler = new IotMqttUpstreamHandler(connectionManager, deviceMessageService, serverId);
107   -
108   - // 初始化下行消息订阅者
109   - IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
110   - IotMqttDownstreamHandler downstreamHandler = new IotMqttDownstreamHandler(deviceMessageService, connectionManager);
111   - this.downstreamSubscriber = new IotMqttDownstreamSubscriber(this, downstreamHandler, messageBus);
112 107 }
113 108  
114 109 @Override
... ... @@ -157,18 +152,13 @@ public class IotMqttProtocol implements IotProtocol {
157 152 getId(), properties.getPort(), serverId);
158 153  
159 154 // 2. 启动下行消息订阅者
  155 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
  156 + IotMqttDownstreamHandler downstreamHandler = new IotMqttDownstreamHandler(deviceMessageService, connectionManager);
  157 + this.downstreamSubscriber = new IotMqttDownstreamSubscriber(this, downstreamHandler, messageBus);
160 158 this.downstreamSubscriber.start();
161 159 } catch (Exception e) {
162 160 log.error("[start][IoT MQTT 协议 {} 启动失败]", getId(), e);
163   - // 启动失败时关闭资源
164   - if (mqttServer != null) {
165   - mqttServer.close();
166   - mqttServer = null;
167   - }
168   - if (vertx != null) {
169   - vertx.close();
170   - vertx = null;
171   - }
  161 + stop0();
172 162 throw e;
173 163 }
174 164 }
... ... @@ -178,15 +168,24 @@ public class IotMqttProtocol implements IotProtocol {
178 168 if (!running) {
179 169 return;
180 170 }
  171 + stop0();
  172 + }
  173 +
  174 + private void stop0() {
181 175 // 1. 停止下行消息订阅者
182   - try {
183   - downstreamSubscriber.stop();
184   - log.info("[stop][IoT MQTT 协议 {} 下行消息订阅者已停止]", getId());
185   - } catch (Exception e) {
186   - log.error("[stop][IoT MQTT 协议 {} 下行消息订阅者停止失败]", getId(), e);
  176 + if (downstreamSubscriber != null) {
  177 + try {
  178 + downstreamSubscriber.stop();
  179 + log.info("[stop][IoT MQTT 协议 {} 下行消息订阅者已停止]", getId());
  180 + } catch (Exception e) {
  181 + log.error("[stop][IoT MQTT 协议 {} 下行消息订阅者停止失败]", getId(), e);
  182 + }
  183 + downstreamSubscriber = null;
187 184 }
188 185  
189   - // 2.1 关闭 MQTT 服务器
  186 + // 2.1 关闭所有连接
  187 + connectionManager.closeAll();
  188 + // 2.2 关闭 MQTT 服务器
190 189 if (mqttServer != null) {
191 190 try {
192 191 mqttServer.close().result();
... ... @@ -196,7 +195,7 @@ public class IotMqttProtocol implements IotProtocol {
196 195 }
197 196 mqttServer = null;
198 197 }
199   - // 2.2 关闭 Vertx 实例
  198 + // 2.3 关闭 Vertx 实例
200 199 if (vertx != null) {
201 200 try {
202 201 vertx.close().result();
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamSubscriber.java
... ... @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream;
2 2  
3 3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5   -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber;
6 6 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol;
7 7 import lombok.extern.slf4j.Slf4j;
8 8  
... ... @@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j;
12 12 * @author 芋道源码
13 13 */
14 14 @Slf4j
15   -public class IotMqttDownstreamSubscriber extends IotProtocolDownstreamSubscriber {
  15 +public class IotMqttDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
16 16  
17 17 private final IotMqttDownstreamHandler downstreamHandler;
18 18  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java
... ... @@ -2,15 +2,13 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream;
2 2  
3 3 import cn.hutool.core.lang.Assert;
4 4 import cn.hutool.core.util.ArrayUtil;
5   -import cn.iocoder.yudao.framework.common.exception.ServiceException;
6 5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7 6 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
8 7 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  8 +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
9 9 import io.vertx.mqtt.MqttEndpoint;
10 10 import lombok.extern.slf4j.Slf4j;
11 11  
12   -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
13   -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
14 12  
15 13 /**
16 14 * IoT 网关 MQTT 上行消息处理器:处理业务消息(属性上报、事件上报等)
... ... @@ -38,10 +36,6 @@ public class IotMqttUpstreamHandler extends IotMqttAbstractHandler {
38 36 */
39 37 public void handleBusinessRequest(MqttEndpoint endpoint, String topic, byte[] payload) {
40 38 String clientId = endpoint.clientIdentifier();
41   - IotDeviceMessage message = null;
42   - String productKey = null;
43   - String deviceName = null;
44   -
45 39 try {
46 40 // 1.1 基础检查
47 41 if (ArrayUtil.isEmpty(payload)) {
... ... @@ -49,8 +43,8 @@ public class IotMqttUpstreamHandler extends IotMqttAbstractHandler {
49 43 }
50 44 // 1.2 解析主题,获取 productKey 和 deviceName
51 45 String[] topicParts = topic.split("/");
52   - productKey = ArrayUtil.get(topicParts, 2);
53   - deviceName = ArrayUtil.get(topicParts, 3);
  46 + String productKey = ArrayUtil.get(topicParts, 2);
  47 + String deviceName = ArrayUtil.get(topicParts, 3);
54 48 Assert.notBlank(productKey, "产品 Key 不能为空");
55 49 Assert.notBlank(deviceName, "设备名称不能为空");
56 50 // 1.3 校验设备信息,防止伪造设备消息
... ... @@ -58,39 +52,27 @@ public class IotMqttUpstreamHandler extends IotMqttAbstractHandler {
58 52 Assert.notNull(connectionInfo, "无法获取连接信息");
59 53 Assert.equals(productKey, connectionInfo.getProductKey(), "产品 Key 不匹配");
60 54 Assert.equals(deviceName, connectionInfo.getDeviceName(), "设备名称不匹配");
  55 + // 1.4 校验 topic 是否允许发布
  56 + if (!IotMqttTopicUtils.isTopicPublishAllowed(topic, productKey, deviceName)) {
  57 + log.warn("[handleBusinessRequest][topic 不允许发布,客户端 ID: {},主题: {}]", clientId, topic);
  58 + return;
  59 + }
61 60  
62   - // 2. 反序列化消息
63   - message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName);
  61 + // 2.1 反序列化消息
  62 + IotDeviceMessage message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName);
64 63 if (message == null) {
65 64 log.warn("[handleBusinessRequest][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic);
66   - sendErrorResponse(endpoint, productKey, deviceName, null, null,
67   - BAD_REQUEST.getCode(), "消息解码失败");
68 65 return;
69 66 }
  67 + // 2.2 标准化回复消息的 method(MQTT 协议中,设备回复消息的 method 会携带 _reply 后缀)
  68 + IotMqttTopicUtils.normalizeReplyMethod(message);
70 69  
71 70 // 3. 处理业务消息
72 71 deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId);
73 72 log.debug("[handleBusinessRequest][消息处理成功,客户端 ID: {},主题: {}]", clientId, topic);
74   - } catch (ServiceException e) {
75   - log.warn("[handleBusinessRequest][业务异常,客户端 ID: {},主题: {},错误: {}]",
76   - clientId, topic, e.getMessage());
77   - String requestId = message != null ? message.getRequestId() : null;
78   - String method = message != null ? message.getMethod() : null;
79   - sendErrorResponse(endpoint, productKey, deviceName, requestId, method, e.getCode(), e.getMessage());
80   - } catch (IllegalArgumentException e) {
81   - log.warn("[handleBusinessRequest][参数校验失败,客户端 ID: {},主题: {},错误: {}]",
82   - clientId, topic, e.getMessage());
83   - String requestId = message != null ? message.getRequestId() : null;
84   - String method = message != null ? message.getMethod() : null;
85   - sendErrorResponse(endpoint, productKey, deviceName, requestId, method,
86   - BAD_REQUEST.getCode(), e.getMessage());
87 73 } catch (Exception e) {
88 74 log.error("[handleBusinessRequest][消息处理异常,客户端 ID: {},主题: {},错误: {}]",
89 75 clientId, topic, e.getMessage(), e);
90   - String requestId = message != null ? message.getRequestId() : null;
91   - String method = message != null ? message.getMethod() : null;
92   - sendErrorResponse(endpoint, productKey, deviceName, requestId, method,
93   - INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
94 76 }
95 77 }
96 78  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java
... ... @@ -8,6 +8,8 @@ import lombok.Data;
8 8 import lombok.extern.slf4j.Slf4j;
9 9 import org.springframework.stereotype.Component;
10 10  
  11 +import java.util.ArrayList;
  12 +import java.util.List;
11 13 import java.util.Map;
12 14 import java.util.concurrent.ConcurrentHashMap;
13 15  
... ... @@ -167,6 +169,24 @@ public class IotMqttConnectionManager {
167 169 }
168 170  
169 171 /**
  172 + * 关闭所有连接
  173 + */
  174 + public void closeAll() {
  175 + // 1. 先复制再清空,避免 closeHandler 回调时并发修改
  176 + List<MqttEndpoint> endpoints = new ArrayList<>(connectionMap.keySet());
  177 + connectionMap.clear();
  178 + deviceEndpointMap.clear();
  179 + // 2. 关闭所有连接(closeHandler 中 unregisterConnection 发现 map 为空会安全跳过)
  180 + for (MqttEndpoint endpoint : endpoints) {
  181 + try {
  182 + endpoint.close();
  183 + } catch (Exception ignored) {
  184 + // 连接可能已关闭,忽略异常
  185 + }
  186 + }
  187 + }
  188 +
  189 + /**
170 190 * 连接信息
171 191 */
172 192 @Data
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpProtocol.java
... ... @@ -66,7 +66,7 @@ public class IotTcpProtocol implements IotProtocol {
66 66 /**
67 67 * 下行消息订阅者
68 68 */
69   - private final IotTcpDownstreamSubscriber downstreamSubscriber;
  69 + private IotTcpDownstreamSubscriber downstreamSubscriber;
70 70  
71 71 /**
72 72 * 消息序列化器
... ... @@ -94,11 +94,6 @@ public class IotTcpProtocol implements IotProtocol {
94 94  
95 95 // 初始化连接管理器
96 96 this.connectionManager = new IotTcpConnectionManager(tcpConfig.getMaxConnections());
97   -
98   - // 初始化下行消息订阅者
99   - IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
100   - IotTcpDownstreamHandler downstreamHandler = new IotTcpDownstreamHandler(connectionManager, frameCodec, serializer);
101   - this.downstreamSubscriber = new IotTcpDownstreamSubscriber(this, downstreamHandler, messageBus);
102 97 }
103 98  
104 99 @Override
... ... @@ -152,18 +147,13 @@ public class IotTcpProtocol implements IotProtocol {
152 147 getId(), properties.getPort(), serverId);
153 148  
154 149 // 2. 启动下行消息订阅者
  150 + IotTcpDownstreamHandler downstreamHandler = new IotTcpDownstreamHandler(connectionManager, frameCodec, serializer);
  151 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
  152 + this.downstreamSubscriber = new IotTcpDownstreamSubscriber(this, downstreamHandler, messageBus);
155 153 this.downstreamSubscriber.start();
156 154 } catch (Exception e) {
157 155 log.error("[start][IoT TCP 协议 {} 启动失败]", getId(), e);
158   - // 启动失败时关闭资源
159   - if (tcpServer != null) {
160   - tcpServer.close();
161   - tcpServer = null;
162   - }
163   - if (vertx != null) {
164   - vertx.close();
165   - vertx = null;
166   - }
  156 + stop0();
167 157 throw e;
168 158 }
169 159 }
... ... @@ -173,15 +163,24 @@ public class IotTcpProtocol implements IotProtocol {
173 163 if (!running) {
174 164 return;
175 165 }
  166 + stop0();
  167 + }
  168 +
  169 + private void stop0() {
176 170 // 1. 停止下行消息订阅者
177   - try {
178   - downstreamSubscriber.stop();
179   - log.info("[stop][IoT TCP 协议 {} 下行消息订阅者已停止]", getId());
180   - } catch (Exception e) {
181   - log.error("[stop][IoT TCP 协议 {} 下行消息订阅者停止失败]", getId(), e);
  171 + if (downstreamSubscriber != null) {
  172 + try {
  173 + downstreamSubscriber.stop();
  174 + log.info("[stop][IoT TCP 协议 {} 下行消息订阅者已停止]", getId());
  175 + } catch (Exception e) {
  176 + log.error("[stop][IoT TCP 协议 {} 下行消息订阅者停止失败]", getId(), e);
  177 + }
  178 + downstreamSubscriber = null;
182 179 }
183 180  
184   - // 2.1 关闭 TCP 服务器
  181 + // 2.1 关闭所有连接
  182 + connectionManager.closeAll();
  183 + // 2.2 关闭 TCP 服务器
185 184 if (tcpServer != null) {
186 185 try {
187 186 tcpServer.close().result();
... ... @@ -191,7 +190,7 @@ public class IotTcpProtocol implements IotProtocol {
191 190 }
192 191 tcpServer = null;
193 192 }
194   - // 2.2 关闭 Vertx 实例
  193 + // 2.3 关闭 Vertx 实例
195 194 if (vertx != null) {
196 195 try {
197 196 vertx.close().result();
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamHandler.java
1 1 package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.downstream;
2 2  
  3 +import cn.hutool.core.util.ObjUtil;
  4 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
3 5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
4 6 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.codec.IotTcpFrameCodec;
5 7 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
... ... @@ -33,9 +35,17 @@ public class IotTcpDownstreamHandler {
33 35 */
34 36 public void handle(IotDeviceMessage message) {
35 37 try {
  38 + // 1.1 检查是否是属性设置消息
  39 + if (ObjUtil.equals(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), message.getMethod())) {
  40 + return;
  41 + }
  42 + if (ObjUtil.notEqual(IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod(), message.getMethod())) {
  43 + log.warn("[handle][忽略非属性设置消息: {}]", message.getMethod());
  44 + return;
  45 + }
36 46 log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]",
37 47 message.getDeviceId(), message.getMethod(), message.getId());
38   - // 1. 检查设备连接
  48 + // 1.2 检查设备连接
39 49 IotTcpConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfoByDeviceId(
40 50 message.getDeviceId());
41 51 if (connectionInfo == null) {
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/handler/downstream/IotTcpDownstreamSubscriber.java
... ... @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.handler.downstream;
3 3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5 5 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
6   -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
  6 +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber;
7 7 import lombok.extern.slf4j.Slf4j;
8 8  
9 9 /**
... ... @@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j;
12 12 * @author 芋道源码
13 13 */
14 14 @Slf4j
15   -public class IotTcpDownstreamSubscriber extends IotProtocolDownstreamSubscriber {
  15 +public class IotTcpDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
16 16  
17 17 private final IotTcpDownstreamHandler downstreamHandler;
18 18  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java
... ... @@ -5,6 +5,8 @@ import io.vertx.core.net.NetSocket;
5 5 import lombok.Data;
6 6 import lombok.extern.slf4j.Slf4j;
7 7  
  8 +import java.util.ArrayList;
  9 +import java.util.List;
8 10 import java.util.Map;
9 11 import java.util.concurrent.ConcurrentHashMap;
10 12  
... ... @@ -123,6 +125,24 @@ public class IotTcpConnectionManager {
123 125 }
124 126  
125 127 /**
  128 + * 关闭所有连接
  129 + */
  130 + public void closeAll() {
  131 + // 1. 先复制再清空,避免 closeHandler 回调时并发修改
  132 + List<NetSocket> sockets = new ArrayList<>(connectionMap.keySet());
  133 + connectionMap.clear();
  134 + deviceSocketMap.clear();
  135 + // 2. 关闭所有连接(closeHandler 中 unregisterConnection 发现 map 为空会安全跳过)
  136 + for (NetSocket socket : sockets) {
  137 + try {
  138 + socket.close();
  139 + } catch (Exception ignored) {
  140 + // 连接可能已关闭,忽略异常
  141 + }
  142 + }
  143 + }
  144 +
  145 + /**
126 146 * 连接信息(包含认证信息)
127 147 */
128 148 @Data
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpProtocol.java
... ... @@ -63,7 +63,7 @@ public class IotUdpProtocol implements IotProtocol {
63 63 /**
64 64 * 下行消息订阅者
65 65 */
66   - private final IotUdpDownstreamSubscriber downstreamSubscriber;
  66 + private IotUdpDownstreamSubscriber downstreamSubscriber;
67 67  
68 68 /**
69 69 * 消息序列化器
... ... @@ -85,10 +85,6 @@ public class IotUdpProtocol implements IotProtocol {
85 85 // 初始化会话管理器
86 86 this.sessionManager = new IotUdpSessionManager(udpConfig.getMaxSessions(), udpConfig.getSessionTimeoutMs());
87 87  
88   - // 初始化下行消息订阅者
89   - IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
90   - IotUdpDownstreamHandler downstreamHandler = new IotUdpDownstreamHandler(this, sessionManager, serializer);
91   - this.downstreamSubscriber = new IotUdpDownstreamSubscriber(this, downstreamHandler, messageBus);
92 88 }
93 89  
94 90 @Override
... ... @@ -108,8 +104,11 @@ public class IotUdpProtocol implements IotProtocol {
108 104 return;
109 105 }
110 106  
111   - // 1.1 创建 Vertx 实例
  107 + // 1.1 创建 Vertx 实例 和 下行消息订阅者
112 108 this.vertx = Vertx.vertx();
  109 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
  110 + IotUdpDownstreamHandler downstreamHandler = new IotUdpDownstreamHandler(this, sessionManager, serializer);
  111 + this.downstreamSubscriber = new IotUdpDownstreamSubscriber(this, downstreamHandler, messageBus);
113 112  
114 113 // 1.2 创建 UDP Socket 选项
115 114 IotUdpConfig udpConfig = properties.getUdp();
... ... @@ -137,15 +136,7 @@ public class IotUdpProtocol implements IotProtocol {
137 136 this.downstreamSubscriber.start();
138 137 } catch (Exception e) {
139 138 log.error("[start][IoT UDP 协议 {} 启动失败]", getId(), e);
140   - // 启动失败时关闭资源
141   - if (udpSocket != null) {
142   - udpSocket.close();
143   - udpSocket = null;
144   - }
145   - if (vertx != null) {
146   - vertx.close();
147   - vertx = null;
148   - }
  139 + stop0();
149 140 throw e;
150 141 }
151 142 }
... ... @@ -155,12 +146,19 @@ public class IotUdpProtocol implements IotProtocol {
155 146 if (!running) {
156 147 return;
157 148 }
  149 + stop0();
  150 + }
  151 +
  152 + private void stop0() {
158 153 // 1. 停止下行消息订阅者
159   - try {
160   - downstreamSubscriber.stop();
161   - log.info("[stop][IoT UDP 协议 {} 下行消息订阅者已停止]", getId());
162   - } catch (Exception e) {
163   - log.error("[stop][IoT UDP 协议 {} 下行消息订阅者停止失败]", getId(), e);
  154 + if (downstreamSubscriber != null) {
  155 + try {
  156 + downstreamSubscriber.stop();
  157 + log.info("[stop][IoT UDP 协议 {} 下行消息订阅者已停止]", getId());
  158 + } catch (Exception e) {
  159 + log.error("[stop][IoT UDP 协议 {} 下行消息订阅者停止失败]", getId(), e);
  160 + }
  161 + downstreamSubscriber = null;
164 162 }
165 163  
166 164 // 2.1 关闭 UDP Socket
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/downstream/IotUdpDownstreamSubscriber.java
... ... @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.udp.handler.downstream;
3 3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5 5 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
6   -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
  6 +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber;
7 7 import lombok.extern.slf4j.Slf4j;
8 8  
9 9 /**
... ... @@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j;
12 12 * @author 芋道源码
13 13 */
14 14 @Slf4j
15   -public class IotUdpDownstreamSubscriber extends IotProtocolDownstreamSubscriber {
  15 +public class IotUdpDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
16 16  
17 17 private final IotUdpDownstreamHandler downstreamHandler;
18 18  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/handler/upstream/IotUdpUpstreamHandler.java
... ... @@ -91,7 +91,6 @@ public class IotUdpUpstreamHandler {
91 91 this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class);
92 92 }
93 93  
94   - // TODO done @AI:vertx 有 udp 的实现么?当前已使用 Vert.x DatagramSocket 实现
95 94 /**
96 95 * 处理 UDP 数据包
97 96 *
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketProtocol.java
1 1 package cn.iocoder.yudao.module.iot.gateway.protocol.websocket;
2 2  
  3 +import cn.hutool.core.lang.Assert;
3 4 import cn.hutool.core.util.ObjUtil;
4 5 import cn.hutool.extra.spring.SpringUtil;
5 6 import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
... ... @@ -9,8 +10,8 @@ import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
9 10 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
10 11 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
11 12 import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
12   -import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream.IotWebSocketDownstreamSubscriber;
13 13 import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream.IotWebSocketDownstreamHandler;
  14 +import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstream.IotWebSocketDownstreamSubscriber;
14 15 import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.upstream.IotWebSocketUpstreamHandler;
15 16 import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager;
16 17 import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
... ... @@ -21,7 +22,6 @@ import io.vertx.core.http.HttpServerOptions;
21 22 import io.vertx.core.net.PemKeyCertOptions;
22 23 import lombok.Getter;
23 24 import lombok.extern.slf4j.Slf4j;
24   -import cn.hutool.core.lang.Assert;
25 25  
26 26 /**
27 27 * IoT WebSocket 协议实现
... ... @@ -65,7 +65,7 @@ public class IotWebSocketProtocol implements IotProtocol {
65 65 /**
66 66 * 下行消息订阅者
67 67 */
68   - private final IotWebSocketDownstreamSubscriber downstreamSubscriber;
  68 + private IotWebSocketDownstreamSubscriber downstreamSubscriber;
69 69  
70 70 /**
71 71 * 消息序列化器
... ... @@ -87,10 +87,6 @@ public class IotWebSocketProtocol implements IotProtocol {
87 87 // 初始化连接管理器
88 88 this.connectionManager = new IotWebSocketConnectionManager();
89 89  
90   - // 初始化下行消息订阅者
91   - IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
92   - IotWebSocketDownstreamHandler downstreamHandler = new IotWebSocketDownstreamHandler(serializer, connectionManager);
93   - this.downstreamSubscriber = new IotWebSocketDownstreamSubscriber(this, downstreamHandler, messageBus);
94 90 }
95 91  
96 92 @Override
... ... @@ -152,17 +148,13 @@ public class IotWebSocketProtocol implements IotProtocol {
152 148 getId(), properties.getPort(), wsConfig.getPath(), serverId);
153 149  
154 150 // 2. 启动下行消息订阅者
  151 + IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
  152 + IotWebSocketDownstreamHandler downstreamHandler = new IotWebSocketDownstreamHandler(serializer, connectionManager);
  153 + this.downstreamSubscriber = new IotWebSocketDownstreamSubscriber(this, downstreamHandler, messageBus);
155 154 downstreamSubscriber.start();
156 155 } catch (Exception e) {
157 156 log.error("[start][IoT WebSocket 协议 {} 启动失败]", getId(), e);
158   - if (httpServer != null) {
159   - httpServer.close();
160   - httpServer = null;
161   - }
162   - if (vertx != null) {
163   - vertx.close();
164   - vertx = null;
165   - }
  157 + stop0();
166 158 throw e;
167 159 }
168 160 }
... ... @@ -172,15 +164,24 @@ public class IotWebSocketProtocol implements IotProtocol {
172 164 if (!running) {
173 165 return;
174 166 }
  167 + stop0();
  168 + }
  169 +
  170 + private void stop0() {
175 171 // 1. 停止下行消息订阅者
176   - try {
177   - downstreamSubscriber.stop();
178   - log.info("[stop][IoT WebSocket 协议 {} 下行消息订阅者已停止]", getId());
179   - } catch (Exception e) {
180   - log.error("[stop][IoT WebSocket 协议 {} 下行消息订阅者停止失败]", getId(), e);
  172 + if (downstreamSubscriber != null) {
  173 + try {
  174 + downstreamSubscriber.stop();
  175 + log.info("[stop][IoT WebSocket 协议 {} 下行消息订阅者已停止]", getId());
  176 + } catch (Exception e) {
  177 + log.error("[stop][IoT WebSocket 协议 {} 下行消息订阅者停止失败]", getId(), e);
  178 + }
  179 + downstreamSubscriber = null;
181 180 }
182 181  
183   - // 2.1 关闭 WebSocket 服务器
  182 + // 2.1 关闭所有连接
  183 + connectionManager.closeAll();
  184 + // 2.2 关闭 WebSocket 服务器
184 185 if (httpServer != null) {
185 186 try {
186 187 httpServer.close().result();
... ... @@ -190,7 +191,7 @@ public class IotWebSocketProtocol implements IotProtocol {
190 191 }
191 192 httpServer = null;
192 193 }
193   - // 2.2 关闭 Vertx 实例
  194 + // 2.3 关闭 Vertx 实例
194 195 if (vertx != null) {
195 196 try {
196 197 vertx.close().result();
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/handler/downstream/IotWebSocketDownstreamSubscriber.java
... ... @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.websocket.handler.downstrea
2 2  
3 3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
4 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5   -import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolDownstreamSubscriber;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber;
6 6 import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketProtocol;
7 7 import lombok.extern.slf4j.Slf4j;
8 8  
... ... @@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j;
12 12 * @author 芋道源码
13 13 */
14 14 @Slf4j
15   -public class IotWebSocketDownstreamSubscriber extends IotProtocolDownstreamSubscriber {
  15 +public class IotWebSocketDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
16 16  
17 17 private final IotWebSocketDownstreamHandler downstreamHandler;
18 18  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/manager/IotWebSocketConnectionManager.java
... ... @@ -5,6 +5,8 @@ import lombok.Data;
5 5 import lombok.experimental.Accessors;
6 6 import lombok.extern.slf4j.Slf4j;
7 7  
  8 +import java.util.ArrayList;
  9 +import java.util.List;
8 10 import java.util.Map;
9 11 import java.util.concurrent.ConcurrentHashMap;
10 12  
... ... @@ -115,6 +117,24 @@ public class IotWebSocketConnectionManager {
115 117 }
116 118  
117 119 /**
  120 + * 关闭所有连接
  121 + */
  122 + public void closeAll() {
  123 + // 1. 先复制再清空,避免 closeHandler 回调时并发修改
  124 + List<ServerWebSocket> sockets = new ArrayList<>(connectionMap.keySet());
  125 + connectionMap.clear();
  126 + deviceSocketMap.clear();
  127 + // 2. 关闭所有连接(closeHandler 中 unregisterConnection 发现 map 为空会安全跳过)
  128 + for (ServerWebSocket socket : sockets) {
  129 + try {
  130 + socket.close();
  131 + } catch (Exception ignored) {
  132 + // 连接可能已关闭,忽略异常
  133 + }
  134 + }
  135 + }
  136 +
  137 + /**
118 138 * 连接信息(包含认证信息)
119 139 */
120 140 @Data
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java
... ... @@ -6,6 +6,8 @@ import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
6 6 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
7 7 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
8 8 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
  9 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO;
  10 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
9 11 import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
10 12 import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
11 13 import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
... ... @@ -24,6 +26,8 @@ import org.springframework.http.ResponseEntity;
24 26 import org.springframework.stereotype.Service;
25 27 import org.springframework.web.client.RestTemplate;
26 28  
  29 +import java.util.List;
  30 +
27 31 import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
28 32  
29 33 /**
... ... @@ -44,7 +48,7 @@ public class IotDeviceApiImpl implements IotDeviceCommonApi {
44 48 public void init() {
45 49 IotGatewayProperties.RpcProperties rpc = gatewayProperties.getRpc();
46 50 restTemplate = new RestTemplateBuilder()
47   - .rootUri(rpc.getUrl() + "/rpc-api/iot/device")
  51 + .rootUri(rpc.getUrl())
48 52 .readTimeout(rpc.getReadTimeout())
49 53 .connectTimeout(rpc.getConnectTimeout())
50 54 .build();
... ... @@ -52,12 +56,17 @@ public class IotDeviceApiImpl implements IotDeviceCommonApi {
52 56  
53 57 @Override
54 58 public CommonResult<Boolean> authDevice(IotDeviceAuthReqDTO authReqDTO) {
55   - return doPost("/auth", authReqDTO, new ParameterizedTypeReference<>() { });
  59 + return doPost("/rpc-api/iot/device/auth", authReqDTO, new ParameterizedTypeReference<>() { });
56 60 }
57 61  
58 62 @Override
59 63 public CommonResult<IotDeviceRespDTO> getDevice(IotDeviceGetReqDTO getReqDTO) {
60   - return doPost("/get", getReqDTO, new ParameterizedTypeReference<>() { });
  64 + return doPost("/rpc-api/iot/device/get", getReqDTO, new ParameterizedTypeReference<>() { });
  65 + }
  66 +
  67 + @Override
  68 + public CommonResult<List<IotModbusDeviceConfigRespDTO>> getModbusDeviceConfigList(IotModbusDeviceConfigListReqDTO listReqDTO) {
  69 + return doPost("/rpc-api/iot/modbus/config-list", listReqDTO, new ParameterizedTypeReference<>() { });
61 70 }
62 71  
63 72 @Override
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java
1 1 package cn.iocoder.yudao.module.iot.gateway.util;
2 2  
3 3 import cn.hutool.core.util.StrUtil;
  4 +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
  5 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
4 6  
5 7 /**
6 8 * IoT 网关 MQTT 主题工具类
... ... @@ -44,6 +46,32 @@ public final class IotMqttTopicUtils {
44 46 */
45 47 public static final String MQTT_ACL_PATH = "/mqtt/acl";
46 48  
  49 + // ========== 消息方法标准化 ==========
  50 +
  51 + /**
  52 + * 标准化设备回复消息的 method
  53 + * <p>
  54 + * MQTT 协议中,设备回复下行指令时,topic 和 method 会携带 _reply 后缀
  55 + * (如 thing.service.invoke_reply)。平台内部统一使用基础 method(如 thing.service.invoke),
  56 + * 通过 {@link IotDeviceMessage#getCode()} 非空来识别回复消息。
  57 + * <p>
  58 + * 此方法剥离 _reply 后缀,并确保 code 字段被设置。
  59 + *
  60 + * @param message 设备消息
  61 + */
  62 + public static void normalizeReplyMethod(IotDeviceMessage message) {
  63 + String method = message.getMethod();
  64 + if (!StrUtil.endWith(method, REPLY_TOPIC_SUFFIX)) {
  65 + return;
  66 + }
  67 + // 1. 剥离 _reply 后缀
  68 + message.setMethod(method.substring(0, method.length() - REPLY_TOPIC_SUFFIX.length()));
  69 + // 2. 确保 code 被设置,使 isReplyMessage() 能正确识别
  70 + if (message.getCode() == null) {
  71 + message.setCode(GlobalErrorCodeConstants.SUCCESS.getCode());
  72 + }
  73 + }
  74 +
47 75 // ========== 工具方法 ==========
48 76  
49 77 /**
... ... @@ -101,8 +129,6 @@ public final class IotMqttTopicUtils {
101 129 * @param deviceName 设备名称
102 130 * @return 是否允许发布
103 131 */
104   - // TODO DONE @AI:这个逻辑,是不是 mqtt 协议,也要使用???答:是通用工具方法,MQTT 协议可按需调用;
105   - // TODO @AI:那你改下 mqtt,也调用!!!
106 132 public static boolean isTopicPublishAllowed(String topic, String productKey, String deviceName) {
107 133 if (!StrUtil.isAllNotBlank(topic, productKey, deviceName)) {
108 134 return false;
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml
... ... @@ -51,8 +51,6 @@ yudao:
51 51 protocol: http
52 52 port: 8092
53 53 enabled: false
54   - http:
55   - ssl-enabled: false
56 54 # ====================================
57 55 # 针对引入的 TCP 组件的配置
58 56 # ====================================
... ... @@ -64,7 +62,6 @@ yudao:
64 62 tcp:
65 63 max-connections: 1000
66 64 keep-alive-timeout-ms: 30000
67   - ssl-enabled: false
68 65 codec:
69 66 type: delimiter # 拆包类型:length_field / delimiter / fixed_length
70 67 delimiter: "\\n" # 分隔符(支持转义:\\n=换行, \\r=回车, \\t=制表符)
... ... @@ -101,7 +98,6 @@ yudao:
101 98 max-message-size: 65536 # 最大消息大小(字节,默认 64KB)
102 99 max-frame-size: 65536 # 最大帧大小(字节,默认 64KB)
103 100 idle-timeout-seconds: 60 # 空闲超时时间(秒,默认 60)
104   - ssl-enabled: false # 是否启用 SSL(wss://)
105 101 # ====================================
106 102 # 针对引入的 CoAP 组件的配置
107 103 # ====================================
... ... @@ -117,14 +113,13 @@ yudao:
117 113 # 针对引入的 MQTT 组件的配置
118 114 # ====================================
119 115 - id: mqtt-json
120   - enabled: true
  116 + enabled: false
121 117 protocol: mqtt
122 118 port: 1883
123 119 serialize: json
124 120 mqtt:
125 121 max-message-size: 8192 # 最大消息大小(字节)
126 122 connect-timeout-seconds: 60 # 连接超时时间(秒)
127   - ssl-enabled: false # 是否启用 SSL
128 123 # ====================================
129 124 # 针对引入的 EMQX 组件的配置
130 125 # ====================================
... ... @@ -168,6 +163,27 @@ yudao:
168 163 key-store-password: "your-keystore-password" # 客户端证书库密码
169 164 trust-store-path: "classpath:certs/trust.jks" # 信任的 CA 证书库路径
170 165 trust-store-password: "your-truststore-password" # 信任的 CA 证书库密码
  166 + # ====================================
  167 + # 针对引入的 Modbus TCP Client 组件的配置
  168 + # ====================================
  169 + - id: modbus-tcp-client-1
  170 + enabled: false
  171 + protocol: modbus_tcp_client
  172 + port: 502
  173 + modbus-tcp-client:
  174 + config-refresh-interval: 30 # 配置刷新间隔(秒)
  175 + # ====================================
  176 + # 针对引入的 Modbus TCP Server 组件的配置
  177 + # ====================================
  178 + - id: modbus-tcp-server-1
  179 + enabled: false
  180 + protocol: modbus_tcp_server
  181 + port: 503
  182 + modbus-tcp-server:
  183 + config-refresh-interval: 30 # 配置刷新间隔(秒)
  184 + custom-function-code: 65 # 自定义功能码(用于认证等扩展交互)
  185 + request-timeout: 5000 # Pending Request 超时时间(毫秒)
  186 + request-cleanup-interval: 10000 # Pending Request 清理间隔(毫秒)
171 187  
172 188 --- #################### 日志相关配置 ####################
173 189  
... ... @@ -189,6 +205,7 @@ logging:
189 205 cn.iocoder.yudao.module.iot.gateway.protocol.mqtt: DEBUG
190 206 cn.iocoder.yudao.module.iot.gateway.protocol.coap: DEBUG
191 207 cn.iocoder.yudao.module.iot.gateway.protocol.websocket: DEBUG
  208 + cn.iocoder.yudao.module.iot.gateway.protocol.modbus: DEBUG
192 209 # 根日志级别
193 210 root: INFO
194 211  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/ModbusRtuOverTcpDemo.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus;
  2 +
  3 +import com.ghgande.j2mod.modbus.io.ModbusRTUTCPTransport;
  4 +import com.ghgande.j2mod.modbus.msg.*;
  5 +import com.ghgande.j2mod.modbus.procimg.*;
  6 +import com.ghgande.j2mod.modbus.slave.ModbusSlave;
  7 +import com.ghgande.j2mod.modbus.slave.ModbusSlaveFactory;
  8 +
  9 +import java.net.ServerSocket;
  10 +import java.net.Socket;
  11 +
  12 +/**
  13 + * Modbus RTU over TCP 完整 Demo
  14 + *
  15 + * 架构:Master(主站)启动 TCP Server 监听 → Slave(从站)主动 TCP 连接上来
  16 + * 通信协议:RTU 帧格式(带 CRC)通过 TCP 传输,而非标准 MBAP 头
  17 + *
  18 + * 流程:
  19 + * 1. Master 启动 TCP ServerSocket 监听端口
  20 + * 2. Slave(从站模拟器)作为 TCP Client 连接到 Master
  21 + * 3. Master 通过 accept 得到的 Socket,使用 {@link ModbusRTUTCPTransport} 发送读写请求
  22 + *
  23 + * 实现说明:
  24 + * 因为 j2mod 的 ModbusSlave 只能以 TCP Server 模式运行(监听端口等待 Master 连接),
  25 + * 不支持"Slave 作为 TCP Client 主动连接 Master"的模式。
  26 + * 所以这里用一个 TCP 桥接(bridge)来模拟:
  27 + * - Slave 在本地内部端口启动(RTU over TCP 模式)
  28 + * - 一个桥接线程同时连接 Master Server 和 Slave 内部端口,做双向数据转发
  29 + * - Master 视角:看到的是 Slave 主动连上来
  30 + *
  31 + * 依赖:j2mod 3.2.1(pom.xml 中已声明)
  32 + *
  33 + * @author 芋道源码
  34 + */
  35 +@Deprecated // 仅技术演示,非是必须的
  36 +public class ModbusRtuOverTcpDemo {
  37 +
  38 + /**
  39 + * Master(主站)TCP Server 监听端口
  40 + */
  41 + private static final int PORT = 5021;
  42 + /**
  43 + * Slave 内部端口(仅本地中转用,不对外暴露)
  44 + */
  45 + private static final int SLAVE_INTERNAL_PORT = PORT + 100;
  46 + /**
  47 + * Modbus 从站地址
  48 + */
  49 + private static final int SLAVE_ID = 1;
  50 +
  51 + public static void main(String[] args) throws Exception {
  52 + // ===================== 第一步:Master 启动 TCP Server 监听 =====================
  53 + ServerSocket serverSocket = new ServerSocket(PORT);
  54 + System.out.println("===================================================");
  55 + System.out.println("[Master] TCP Server 已启动,监听端口: " + PORT);
  56 + System.out.println("[Master] 等待 Slave 连接...");
  57 + System.out.println("===================================================");
  58 +
  59 + // ===================== 第二步:后台启动 Slave,它会主动连接 Master =====================
  60 + ModbusSlave slave = startSlaveInBackground();
  61 +
  62 + // Master accept Slave 的连接
  63 + Socket slaveSocket = serverSocket.accept();
  64 + System.out.println("[Master] Slave 已连接: " + slaveSocket.getRemoteSocketAddress());
  65 +
  66 + // ===================== 第三步:Master 通过 RTU over TCP 发送读写请求 =====================
  67 + // 使用 ModbusRTUTCPTransport 包装 Socket(RTU 帧 = SlaveID + 功能码 + 数据 + CRC,无 MBAP 头)
  68 + ModbusRTUTCPTransport transport = new ModbusRTUTCPTransport(slaveSocket);
  69 +
  70 + try {
  71 + System.out.println("[Master] RTU over TCP 通道已建立\n");
  72 +
  73 + // 1. 读操作演示:4 种功能码
  74 + demoReadCoils(transport); // 功能码 01:读线圈
  75 + demoReadDiscreteInputs(transport); // 功能码 02:读离散输入
  76 + demoReadHoldingRegisters(transport); // 功能码 03:读保持寄存器
  77 + demoReadInputRegisters(transport); // 功能码 04:读输入寄存器
  78 +
  79 + // 2. 写操作演示 + 读回验证
  80 + demoWriteCoil(transport); // 功能码 05:写单个线圈
  81 + demoWriteRegister(transport); // 功能码 06:写单个保持寄存器
  82 +
  83 + System.out.println("\n===================================================");
  84 + System.out.println("所有 RTU over TCP 读写操作执行成功!");
  85 + System.out.println("===================================================");
  86 + } finally {
  87 + // 清理资源
  88 + transport.close();
  89 + slaveSocket.close();
  90 + serverSocket.close();
  91 + slave.close();
  92 + System.out.println("[Master] 资源已关闭");
  93 + }
  94 + }
  95 +
  96 + // ===================== Slave 设备模拟(作为 TCP Client 连接 Master) =====================
  97 +
  98 + /**
  99 + * 在后台启动从站模拟器,并通过 TCP 桥接连到 Master Server
  100 + *
  101 + * @return ModbusSlave 实例(用于最后关闭资源)
  102 + */
  103 + private static ModbusSlave startSlaveInBackground() throws Exception {
  104 + // 1. 创建进程映像,初始化寄存器数据
  105 + SimpleProcessImage spi = new SimpleProcessImage(SLAVE_ID);
  106 + // 1.1 线圈(Coil,功能码 01/05)- 可读写,地址 0~9
  107 + for (int i = 0; i < 10; i++) {
  108 + spi.addDigitalOut(new SimpleDigitalOut(i % 2 == 0));
  109 + }
  110 + // 1.2 离散输入(Discrete Input,功能码 02)- 只读,地址 0~9
  111 + for (int i = 0; i < 10; i++) {
  112 + spi.addDigitalIn(new SimpleDigitalIn(i % 3 == 0));
  113 + }
  114 + // 1.3 保持寄存器(Holding Register,功能码 03/06/16)- 可读写,地址 0~19
  115 + for (int i = 0; i < 20; i++) {
  116 + spi.addRegister(new SimpleRegister(i * 100));
  117 + }
  118 + // 1.4 输入寄存器(Input Register,功能码 04)- 只读,地址 0~19
  119 + for (int i = 0; i < 20; i++) {
  120 + spi.addInputRegister(new SimpleInputRegister(i * 10 + 1));
  121 + }
  122 +
  123 + // 2. 启动 Slave(RTU over TCP 模式,在本地内部端口监听)
  124 + ModbusSlave slave = ModbusSlaveFactory.createTCPSlave(SLAVE_INTERNAL_PORT, 5, true);
  125 + slave.addProcessImage(SLAVE_ID, spi);
  126 + slave.open();
  127 + System.out.println("[Slave] 从站模拟器已启动(内部端口: " + SLAVE_INTERNAL_PORT + ")");
  128 +
  129 + // 3. 启动桥接线程:TCP Client 连接 Master Server,同时连接 Slave 内部端口,双向转发
  130 + Thread bridgeThread = new Thread(() -> {
  131 + try {
  132 + Socket toMaster = new Socket("127.0.0.1", PORT);
  133 + Socket toSlave = new Socket("127.0.0.1", SLAVE_INTERNAL_PORT);
  134 + System.out.println("[Bridge] 已建立桥接: Master(" + PORT + ") <-> Slave(" + SLAVE_INTERNAL_PORT + ")");
  135 +
  136 + // 双向桥接:Master ↔ Bridge ↔ Slave
  137 + Thread forward = new Thread(() -> bridge(toMaster, toSlave), "bridge-master→slave");
  138 + Thread backward = new Thread(() -> bridge(toSlave, toMaster), "bridge-slave→master");
  139 + forward.setDaemon(true);
  140 + backward.setDaemon(true);
  141 + forward.start();
  142 + backward.start();
  143 + } catch (Exception e) {
  144 + e.printStackTrace();
  145 + }
  146 + }, "bridge-setup");
  147 + bridgeThread.setDaemon(true);
  148 + bridgeThread.start();
  149 +
  150 + return slave;
  151 + }
  152 +
  153 + /**
  154 + * TCP 双向桥接:从 src 读取数据,写入 dst
  155 + */
  156 + private static void bridge(Socket src, Socket dst) {
  157 + try {
  158 + byte[] buf = new byte[1024];
  159 + var in = src.getInputStream();
  160 + var out = dst.getOutputStream();
  161 + int len;
  162 + while ((len = in.read(buf)) != -1) {
  163 + out.write(buf, 0, len);
  164 + out.flush();
  165 + }
  166 + } catch (Exception ignored) {
  167 + // 连接关闭时正常退出
  168 + }
  169 + }
  170 +
  171 + // ===================== Master 读写操作 =====================
  172 +
  173 + /**
  174 + * 发送请求并接收响应(通用方法)
  175 + */
  176 + private static ModbusResponse sendRequest(ModbusRTUTCPTransport transport, ModbusRequest request) throws Exception {
  177 + request.setUnitID(SLAVE_ID);
  178 + transport.writeRequest(request);
  179 + return transport.readResponse();
  180 + }
  181 +
  182 + /**
  183 + * 功能码 01:读线圈(Read Coils)
  184 + */
  185 + private static void demoReadCoils(ModbusRTUTCPTransport transport) throws Exception {
  186 + ReadCoilsRequest request = new ReadCoilsRequest(0, 5);
  187 + ReadCoilsResponse response = (ReadCoilsResponse) sendRequest(transport, request);
  188 +
  189 + StringBuilder sb = new StringBuilder("[功能码 01] 读线圈(0~4): ");
  190 + for (int i = 0; i < 5; i++) {
  191 + sb.append(response.getCoilStatus(i) ? "ON" : "OFF");
  192 + if (i < 4) {
  193 + sb.append(", ");
  194 + }
  195 + }
  196 + System.out.println(sb);
  197 + }
  198 +
  199 + /**
  200 + * 功能码 02:读离散输入(Read Discrete Inputs)
  201 + */
  202 + private static void demoReadDiscreteInputs(ModbusRTUTCPTransport transport) throws Exception {
  203 + ReadInputDiscretesRequest request = new ReadInputDiscretesRequest(0, 5);
  204 + ReadInputDiscretesResponse response = (ReadInputDiscretesResponse) sendRequest(transport, request);
  205 +
  206 + StringBuilder sb = new StringBuilder("[功能码 02] 读离散输入(0~4): ");
  207 + for (int i = 0; i < 5; i++) {
  208 + sb.append(response.getDiscreteStatus(i) ? "ON" : "OFF");
  209 + if (i < 4) {
  210 + sb.append(", ");
  211 + }
  212 + }
  213 + System.out.println(sb);
  214 + }
  215 +
  216 + /**
  217 + * 功能码 03:读保持寄存器(Read Holding Registers)
  218 + */
  219 + private static void demoReadHoldingRegisters(ModbusRTUTCPTransport transport) throws Exception {
  220 + ReadMultipleRegistersRequest request = new ReadMultipleRegistersRequest(0, 5);
  221 + ReadMultipleRegistersResponse response = (ReadMultipleRegistersResponse) sendRequest(transport, request);
  222 +
  223 + StringBuilder sb = new StringBuilder("[功能码 03] 读保持寄存器(0~4): ");
  224 + for (int i = 0; i < response.getWordCount(); i++) {
  225 + sb.append(response.getRegisterValue(i));
  226 + if (i < response.getWordCount() - 1) {
  227 + sb.append(", ");
  228 + }
  229 + }
  230 + System.out.println(sb);
  231 + }
  232 +
  233 + /**
  234 + * 功能码 04:读输入寄存器(Read Input Registers)
  235 + */
  236 + private static void demoReadInputRegisters(ModbusRTUTCPTransport transport) throws Exception {
  237 + ReadInputRegistersRequest request = new ReadInputRegistersRequest(0, 5);
  238 + ReadInputRegistersResponse response = (ReadInputRegistersResponse) sendRequest(transport, request);
  239 +
  240 + StringBuilder sb = new StringBuilder("[功能码 04] 读输入寄存器(0~4): ");
  241 + for (int i = 0; i < response.getWordCount(); i++) {
  242 + sb.append(response.getRegisterValue(i));
  243 + if (i < response.getWordCount() - 1) {
  244 + sb.append(", ");
  245 + }
  246 + }
  247 + System.out.println(sb);
  248 + }
  249 +
  250 + /**
  251 + * 功能码 05:写单个线圈(Write Single Coil)+ 读回验证
  252 + */
  253 + private static void demoWriteCoil(ModbusRTUTCPTransport transport) throws Exception {
  254 + int address = 0;
  255 +
  256 + // 1. 先读取当前值
  257 + ReadCoilsRequest readReq = new ReadCoilsRequest(address, 1);
  258 + ReadCoilsResponse readResp = (ReadCoilsResponse) sendRequest(transport, readReq);
  259 + boolean beforeValue = readResp.getCoilStatus(0);
  260 +
  261 + // 2. 写入相反的值
  262 + boolean writeValue = !beforeValue;
  263 + WriteCoilRequest writeReq = new WriteCoilRequest(address, writeValue);
  264 + sendRequest(transport, writeReq);
  265 +
  266 + // 3. 读回验证
  267 + ReadCoilsResponse verifyResp = (ReadCoilsResponse) sendRequest(transport, readReq);
  268 + boolean afterValue = verifyResp.getCoilStatus(0);
  269 +
  270 + System.out.println("[功能码 05] 写线圈: 地址=" + address
  271 + + ", 写入前=" + (beforeValue ? "ON" : "OFF")
  272 + + ", 写入值=" + (writeValue ? "ON" : "OFF")
  273 + + ", 读回值=" + (afterValue ? "ON" : "OFF")
  274 + + (afterValue == writeValue ? " ✓ 验证通过" : " ✗ 验证失败"));
  275 + }
  276 +
  277 + /**
  278 + * 功能码 06:写单个保持寄存器(Write Single Register)+ 读回验证
  279 + */
  280 + private static void demoWriteRegister(ModbusRTUTCPTransport transport) throws Exception {
  281 + int address = 0;
  282 + int writeValue = 12345;
  283 +
  284 + // 1. 先读取当前值
  285 + ReadMultipleRegistersRequest readReq = new ReadMultipleRegistersRequest(address, 1);
  286 + ReadMultipleRegistersResponse readResp = (ReadMultipleRegistersResponse) sendRequest(transport, readReq);
  287 + int beforeValue = readResp.getRegisterValue(0);
  288 +
  289 + // 2. 写入新值
  290 + WriteSingleRegisterRequest writeReq = new WriteSingleRegisterRequest(address, new SimpleRegister(writeValue));
  291 + sendRequest(transport, writeReq);
  292 +
  293 + // 3. 读回验证
  294 + ReadMultipleRegistersResponse verifyResp = (ReadMultipleRegistersResponse) sendRequest(transport, readReq);
  295 + int afterValue = verifyResp.getRegisterValue(0);
  296 +
  297 + System.out.println("[功能码 06] 写保持寄存器: 地址=" + address
  298 + + ", 写入前=" + beforeValue
  299 + + ", 写入值=" + writeValue
  300 + + ", 读回值=" + afterValue
  301 + + (afterValue == writeValue ? " ✓ 验证通过" : " ✗ 验证失败"));
  302 + }
  303 +
  304 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpclient/IoTModbusTcpClientIntegrationTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient;
  2 +
  3 +import com.ghgande.j2mod.modbus.procimg.*;
  4 +import com.ghgande.j2mod.modbus.slave.ModbusSlave;
  5 +import com.ghgande.j2mod.modbus.slave.ModbusSlaveFactory;
  6 +import lombok.extern.slf4j.Slf4j;
  7 +import org.junit.jupiter.api.Disabled;
  8 +import org.junit.jupiter.api.Test;
  9 +
  10 +/**
  11 + * Modbus TCP 从站模拟器(手动测试)
  12 + *
  13 + * <p>测试场景:模拟一个标准 Modbus TCP 从站设备,供 Modbus TCP Client 网关连接和读写数据
  14 + *
  15 + * <p>使用步骤:
  16 + * <ol>
  17 + * <li>运行 {@link #testStartSlaveSimulator()} 启动模拟从站(默认端口 5020,从站地址 1)</li>
  18 + * <li>启动 yudao-module-iot-gateway 服务(需开启 modbus-tcp-client 协议)</li>
  19 + * <li>确保数据库有对应的 Modbus Client 设备配置(ip=127.0.0.1, port=5020, slaveId=1)</li>
  20 + * <li>网关会自动连接模拟从站并开始轮询读取寄存器数据</li>
  21 + * <li>模拟器每 5 秒自动更新输入寄存器和保持寄存器的值,模拟传感器数据变化</li>
  22 + * </ol>
  23 + *
  24 + * <p>可用寄存器:
  25 + * <ul>
  26 + * <li>线圈 (Coil, 功能码 01/05): 地址 0-9,交替 true/false</li>
  27 + * <li>离散输入 (Discrete Input, 功能码 02): 地址 0-9,每 3 个一个 true</li>
  28 + * <li>保持寄存器 (Holding Register, 功能码 03/06/16): 地址 0-19,初始值 0,100,200,...</li>
  29 + * <li>输入寄存器 (Input Register, 功能码 04): 地址 0-19,初始值 1,11,21,...</li>
  30 + * </ul>
  31 + *
  32 + * @author 芋道源码
  33 + */
  34 +@Slf4j
  35 +@Disabled
  36 +public class IoTModbusTcpClientIntegrationTest {
  37 +
  38 + private static final int PORT = 5020;
  39 + private static final int SLAVE_ID = 1;
  40 +
  41 + /**
  42 + * 启动 Modbus TCP 从站模拟器
  43 + *
  44 + * <p>模拟器会持续运行,每 5 秒更新一次寄存器数据,直到手动停止
  45 + */
  46 + @SuppressWarnings({"InfiniteLoopStatement", "BusyWait"})
  47 + @Test
  48 + public void testStartSlaveSimulator() throws Exception {
  49 + // 1. 创建进程映像(Process Image),存储寄存器数据
  50 + SimpleProcessImage spi = new SimpleProcessImage(SLAVE_ID);
  51 +
  52 + // 2.1 初始化线圈(Coil,功能码 01/05)- 离散输出,可读写
  53 + // 地址 0-9,共 10 个线圈
  54 + for (int i = 0; i < 10; i++) {
  55 + spi.addDigitalOut(new SimpleDigitalOut(i % 2 == 0)); // 交替 true/false
  56 + }
  57 +
  58 + // 2.2 初始化离散输入(Discrete Input,功能码 02)- 只读
  59 + // 地址 0-9,共 10 个离散输入
  60 + for (int i = 0; i < 10; i++) {
  61 + spi.addDigitalIn(new SimpleDigitalIn(i % 3 == 0)); // 每 3 个一个 true
  62 + }
  63 +
  64 + // 2.3 初始化保持寄存器(Holding Register,功能码 03/06/16)- 可读写
  65 + // 地址 0-19,共 20 个寄存器
  66 + for (int i = 0; i < 20; i++) {
  67 + spi.addRegister(new SimpleRegister(i * 100)); // 值为 0, 100, 200, ...
  68 + }
  69 +
  70 + // 2.4 初始化输入寄存器(Input Register,功能码 04)- 只读
  71 + // 地址 0-19,共 20 个寄存器
  72 + SimpleInputRegister[] inputRegisters = new SimpleInputRegister[20];
  73 + for (int i = 0; i < 20; i++) {
  74 + inputRegisters[i] = new SimpleInputRegister(i * 10 + 1); // 值为 1, 11, 21, ...
  75 + spi.addInputRegister(inputRegisters[i]);
  76 + }
  77 +
  78 + // 3.1 创建 Modbus TCP 从站
  79 + ModbusSlave slave = ModbusSlaveFactory.createTCPSlave(PORT, 5);
  80 + slave.addProcessImage(SLAVE_ID, spi);
  81 + // 3.2 启动从站
  82 + slave.open();
  83 +
  84 + log.info("[testStartSlaveSimulator][Modbus TCP 从站模拟器已启动, 端口: {}, 从站地址: {}]", PORT, SLAVE_ID);
  85 + log.info("[testStartSlaveSimulator][可用寄存器: 线圈(01/05) 0-9, 离散输入(02) 0-9, " +
  86 + "保持寄存器(03/06/16) 0-19, 输入寄存器(04) 0-19]");
  87 +
  88 + // 4. 添加关闭钩子
  89 + Runtime.getRuntime().addShutdownHook(new Thread(() -> {
  90 + log.info("[testStartSlaveSimulator][正在关闭模拟器...]");
  91 + slave.close();
  92 + log.info("[testStartSlaveSimulator][模拟器已关闭]");
  93 + }));
  94 +
  95 + // 5. 保持运行,定时更新输入寄存器模拟数据变化
  96 + int counter = 0;
  97 + while (true) {
  98 + Thread.sleep(5000); // 每 5 秒更新一次
  99 + counter++;
  100 +
  101 + // 更新输入寄存器的值,模拟传感器数据变化
  102 + for (int i = 0; i < 20; i++) {
  103 + int newValue = (i * 10 + 1) + counter;
  104 + inputRegisters[i].setValue(newValue);
  105 + }
  106 +
  107 + // 更新保持寄存器的第一个值
  108 + spi.getRegister(0).setValue(counter * 100);
  109 + log.info("[testStartSlaveSimulator][数据已更新, counter={}, 保持寄存器[0]={}, 输入寄存器[0]={}]",
  110 + counter, counter * 100, 1 + counter);
  111 + }
  112 + }
  113 +
  114 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerRtuIntegrationTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver;
  2 +
  3 +import cn.hutool.core.util.HexUtil;
  4 +import cn.hutool.json.JSONObject;
  5 +import cn.hutool.json.JSONUtil;
  6 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
  7 +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum;
  8 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  9 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
  10 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrame;
  11 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameDecoder;
  12 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameEncoder;
  13 +import io.vertx.core.Vertx;
  14 +import io.vertx.core.buffer.Buffer;
  15 +import io.vertx.core.net.NetClient;
  16 +import io.vertx.core.net.NetClientOptions;
  17 +import io.vertx.core.net.NetSocket;
  18 +import io.vertx.core.parsetools.RecordParser;
  19 +import lombok.extern.slf4j.Slf4j;
  20 +import org.junit.jupiter.api.AfterAll;
  21 +import org.junit.jupiter.api.BeforeAll;
  22 +import org.junit.jupiter.api.Disabled;
  23 +import org.junit.jupiter.api.Test;
  24 +
  25 +import java.util.concurrent.CompletableFuture;
  26 +import java.util.concurrent.TimeUnit;
  27 +
  28 +import static org.junit.jupiter.api.Assertions.assertEquals;
  29 +
  30 +/**
  31 + * IoT Modbus TCP Server 协议集成测试 — MODBUS_RTU 帧格式(手动测试)
  32 + *
  33 + * <p>测试场景:设备(TCP Client)连接到网关(TCP Server),使用 MODBUS_RTU(CRC16)帧格式通信
  34 + *
  35 + * <p>使用步骤:
  36 + * <ol>
  37 + * <li>启动 yudao-module-iot-gateway 服务(需开启 modbus-tcp-server 协议,默认端口 503)</li>
  38 + * <li>确保数据库有对应的 Modbus 设备配置(mode=1, frameFormat=modbus_rtu)</li>
  39 + * <li>运行以下测试方法:
  40 + * <ul>
  41 + * <li>{@link #testAuth()} - 自定义功能码认证</li>
  42 + * <li>{@link #testPollingResponse()} - 轮询响应</li>
  43 + * <li>{@link #testPropertySetWrite()} - 属性设置(接收写指令)</li>
  44 + * </ul>
  45 + * </li>
  46 + * </ol>
  47 + *
  48 + * @author 芋道源码
  49 + */
  50 +@Slf4j
  51 +@Disabled
  52 +public class IotModbusTcpServerRtuIntegrationTest {
  53 +
  54 + private static final String SERVER_HOST = "127.0.0.1";
  55 + private static final int SERVER_PORT = 503;
  56 + private static final int TIMEOUT_MS = 5000;
  57 +
  58 + private static final int CUSTOM_FC = 65;
  59 + private static final int SLAVE_ID = 1;
  60 +
  61 + private static Vertx vertx;
  62 + private static NetClient netClient;
  63 +
  64 + // ===================== 编解码器 =====================
  65 +
  66 + private static final IotModbusFrameDecoder FRAME_DECODER = new IotModbusFrameDecoder(CUSTOM_FC);
  67 + private static final IotModbusFrameEncoder FRAME_ENCODER = new IotModbusFrameEncoder(CUSTOM_FC);
  68 +
  69 + // ===================== 设备信息(根据实际情况修改,从 iot_device 表查询) =====================
  70 +
  71 + private static final String PRODUCT_KEY = "modbus_tcp_server_product_demo";
  72 + private static final String DEVICE_NAME = "modbus_tcp_server_device_demo_rtu";
  73 + private static final String DEVICE_SECRET = "af01c55eb8e3424bb23fc6c783936b2e";
  74 +
  75 + @BeforeAll
  76 + static void setUp() {
  77 + vertx = Vertx.vertx();
  78 + NetClientOptions options = new NetClientOptions()
  79 + .setConnectTimeout(TIMEOUT_MS)
  80 + .setIdleTimeout(TIMEOUT_MS);
  81 + netClient = vertx.createNetClient(options);
  82 + }
  83 +
  84 + @AfterAll
  85 + static void tearDown() {
  86 + if (netClient != null) {
  87 + netClient.close();
  88 + }
  89 + if (vertx != null) {
  90 + vertx.close();
  91 + }
  92 + }
  93 +
  94 + // ===================== 认证测试 =====================
  95 +
  96 + /**
  97 + * 认证测试:发送自定义功能码 FC65 认证帧(RTU 格式),验证认证成功响应
  98 + */
  99 + @Test
  100 + public void testAuth() throws Exception {
  101 + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
  102 + try {
  103 + // 1. 构造并发送认证帧
  104 + IotModbusFrame response = authenticate(socket);
  105 +
  106 + // 2. 验证响应
  107 + log.info("[testAuth][认证响应帧: slaveId={}, FC={}, customData={}]",
  108 + response.getSlaveId(), response.getFunctionCode(), response.getCustomData());
  109 + JSONObject json = JSONUtil.parseObj(response.getCustomData());
  110 + assertEquals(0, json.getInt("code"));
  111 + log.info("[testAuth][认证结果: code={}, message={}]", json.getInt("code"), json.getStr("message"));
  112 + } finally {
  113 + socket.close();
  114 + }
  115 + }
  116 +
  117 + // ===================== 轮询响应测试 =====================
  118 +
  119 + /**
  120 + * 轮询响应测试:认证后持续监听网关下发的读请求(RTU 格式),每次收到都自动构造读响应帧发回
  121 + */
  122 + @Test
  123 + public void testPollingResponse() throws Exception {
  124 + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
  125 + try {
  126 + // 1. 先认证
  127 + IotModbusFrame authResponse = authenticate(socket);
  128 + log.info("[testPollingResponse][认证响应: {}]", authResponse.getCustomData());
  129 + JSONObject authJson = JSONUtil.parseObj(authResponse.getCustomData());
  130 + assertEquals(0, authJson.getInt("code"));
  131 +
  132 + // 2. 设置持续监听:每收到一个读请求,自动回复
  133 + log.info("[testPollingResponse][开始持续监听网关下发的读请求...]");
  134 + CompletableFuture<Void> done = new CompletableFuture<>();
  135 + // 注意:使用 requestMode=true,因为设备端收到的是网关下发的读请求(非响应)
  136 + RecordParser parser = FRAME_DECODER.createRecordParser((frame, frameFormat) -> {
  137 + log.info("[testPollingResponse][收到请求: slaveId={}, FC={}]",
  138 + frame.getSlaveId(), frame.getFunctionCode());
  139 + // 解析读请求中的起始地址和数量
  140 + byte[] pdu = frame.getPdu();
  141 + int startAddress = ((pdu[0] & 0xFF) << 8) | (pdu[1] & 0xFF);
  142 + int quantity = ((pdu[2] & 0xFF) << 8) | (pdu[3] & 0xFF);
  143 + log.info("[testPollingResponse][读请求参数: startAddress={}, quantity={}]", startAddress, quantity);
  144 +
  145 + // 构造读响应帧(模拟寄存器数据,RTU 格式)
  146 + int[] registerValues = new int[quantity];
  147 + for (int i = 0; i < quantity; i++) {
  148 + registerValues[i] = 100 + i * 100; // 模拟值: 100, 200, 300, ...
  149 + }
  150 + byte[] responseData = buildReadResponse(frame.getSlaveId(),
  151 + frame.getFunctionCode(), registerValues);
  152 + socket.write(Buffer.buffer(responseData));
  153 + log.info("[testPollingResponse][已发送读响应, registerValues={}]", registerValues);
  154 + }, true);
  155 + socket.handler(parser);
  156 +
  157 + // 3. 持续等待(200 秒),期间会自动回复所有收到的读请求
  158 + Thread.sleep(200000);
  159 + } finally {
  160 + socket.close();
  161 + }
  162 + }
  163 +
  164 + // ===================== 属性设置测试 =====================
  165 +
  166 + /**
  167 + * 属性设置测试:认证后等待接收网关下发的 FC06/FC16 写请求(RTU 格式)
  168 + * <p>
  169 + * 注意:需手动在平台触发 property.set
  170 + */
  171 + @Test
  172 + public void testPropertySetWrite() throws Exception {
  173 + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
  174 + try {
  175 + // 1. 先认证
  176 + IotModbusFrame authResponse = authenticate(socket);
  177 + log.info("[testPropertySetWrite][认证响应: {}]", authResponse.getCustomData());
  178 +
  179 + // 2. 等待网关下发写请求(需手动在平台触发 property.set)
  180 + log.info("[testPropertySetWrite][等待网关下发写请求(请在平台触发 property.set)...]");
  181 + IotModbusFrame writeRequest = waitForRequest(socket);
  182 + log.info("[testPropertySetWrite][收到写请求: slaveId={}, FC={}, pdu={}]",
  183 + writeRequest.getSlaveId(), writeRequest.getFunctionCode(),
  184 + HexUtil.encodeHexStr(writeRequest.getPdu()));
  185 + } finally {
  186 + socket.close();
  187 + }
  188 + }
  189 +
  190 + // ===================== 辅助方法 =====================
  191 +
  192 + /**
  193 + * 建立 TCP 连接
  194 + */
  195 + private CompletableFuture<NetSocket> connect() {
  196 + CompletableFuture<NetSocket> future = new CompletableFuture<>();
  197 + netClient.connect(SERVER_PORT, SERVER_HOST)
  198 + .onSuccess(future::complete)
  199 + .onFailure(future::completeExceptionally);
  200 + return future;
  201 + }
  202 +
  203 + /**
  204 + * 执行认证并返回响应帧
  205 + */
  206 + private IotModbusFrame authenticate(NetSocket socket) throws Exception {
  207 + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
  208 + authInfo.setClientId("");
  209 + byte[] authFrame = buildAuthFrame(authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword());
  210 + return sendAndReceive(socket, authFrame);
  211 + }
  212 +
  213 + /**
  214 + * 发送帧并等待响应(使用 IotModbusFrameDecoder 自动检测帧格式并解码)
  215 + */
  216 + private IotModbusFrame sendAndReceive(NetSocket socket, byte[] frameData) throws Exception {
  217 + CompletableFuture<IotModbusFrame> responseFuture = new CompletableFuture<>();
  218 + // 使用 FrameDecoder 创建拆包器(自动检测帧格式 + 解码,直接回调 IotModbusFrame)
  219 + RecordParser parser = FRAME_DECODER.createRecordParser(
  220 + (frame, frameFormat) -> {
  221 + try {
  222 + log.info("[sendAndReceive][检测到帧格式: {}]", frameFormat);
  223 + responseFuture.complete(frame);
  224 + } catch (Exception e) {
  225 + responseFuture.completeExceptionally(e);
  226 + }
  227 + });
  228 + socket.handler(parser);
  229 +
  230 + // 发送请求
  231 + log.info("[sendAndReceive][发送帧, 长度={}]", frameData.length);
  232 + socket.write(Buffer.buffer(frameData));
  233 +
  234 + // 等待响应
  235 + return responseFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
  236 + }
  237 +
  238 + /**
  239 + * 等待接收网关下发的请求帧(不发送,只等待接收)
  240 + */
  241 + private IotModbusFrame waitForRequest(NetSocket socket) throws Exception {
  242 + CompletableFuture<IotModbusFrame> requestFuture = new CompletableFuture<>();
  243 + // 使用 FrameDecoder 创建拆包器(直接回调 IotModbusFrame)
  244 + RecordParser parser = FRAME_DECODER.createRecordParser(
  245 + (frame, frameFormat) -> {
  246 + try {
  247 + log.info("[waitForRequest][检测到帧格式: {}]", frameFormat);
  248 + requestFuture.complete(frame);
  249 + } catch (Exception e) {
  250 + requestFuture.completeExceptionally(e);
  251 + }
  252 + });
  253 + socket.handler(parser);
  254 +
  255 + // 等待(超时 30 秒,因为轮询间隔可能比较长)
  256 + return requestFuture.get(30000, TimeUnit.MILLISECONDS);
  257 + }
  258 +
  259 + /**
  260 + * 构造认证帧(MODBUS_RTU 格式)
  261 + * <p>
  262 + * JSON: {"method":"auth","params":{"clientId":"...","username":"...","password":"..."}}
  263 + * <p>
  264 + * RTU 帧格式:[SlaveId(1)] [FC=0x41(1)] [ByteCount(1)] [JSON(N)] [CRC16(2)]
  265 + */
  266 + private byte[] buildAuthFrame(String clientId, String username, String password) {
  267 + JSONObject params = new JSONObject();
  268 + params.set("clientId", clientId);
  269 + params.set("username", username);
  270 + params.set("password", password);
  271 + JSONObject json = new JSONObject();
  272 + json.set("method", "auth");
  273 + json.set("params", params);
  274 + return FRAME_ENCODER.encodeCustomFrame(SLAVE_ID, json.toString(),
  275 + IotModbusFrameFormatEnum.MODBUS_RTU, 0);
  276 + }
  277 +
  278 + /**
  279 + * 构造 FC03/FC01-04 读响应帧(MODBUS_RTU 格式)
  280 + * <p>
  281 + * RTU 帧格式:[SlaveId(1)] [FC(1)] [ByteCount(1)] [RegisterData(N*2)] [CRC16(2)]
  282 + */
  283 + private byte[] buildReadResponse(int slaveId, int functionCode, int[] registerValues) {
  284 + int byteCount = registerValues.length * 2;
  285 + // 帧长度:SlaveId(1) + FC(1) + ByteCount(1) + Data(N*2) + CRC(2)
  286 + int totalLength = 1 + 1 + 1 + byteCount + 2;
  287 + byte[] frame = new byte[totalLength];
  288 + frame[0] = (byte) slaveId;
  289 + frame[1] = (byte) functionCode;
  290 + frame[2] = (byte) byteCount;
  291 + for (int i = 0; i < registerValues.length; i++) {
  292 + frame[3 + i * 2] = (byte) ((registerValues[i] >> 8) & 0xFF);
  293 + frame[3 + i * 2 + 1] = (byte) (registerValues[i] & 0xFF);
  294 + }
  295 + // 计算 CRC16
  296 + int crc = IotModbusCommonUtils.calculateCrc16(frame, totalLength - 2);
  297 + frame[totalLength - 2] = (byte) (crc & 0xFF); // CRC Low
  298 + frame[totalLength - 1] = (byte) ((crc >> 8) & 0xFF); // CRC High
  299 + return frame;
  300 + }
  301 +
  302 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerTcpIntegrationTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver;
  2 +
  3 +import cn.hutool.core.util.HexUtil;
  4 +import cn.hutool.json.JSONObject;
  5 +import cn.hutool.json.JSONUtil;
  6 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
  7 +import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusFrameFormatEnum;
  8 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
  9 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrame;
  10 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameDecoder;
  11 +import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrameEncoder;
  12 +import io.vertx.core.Vertx;
  13 +import io.vertx.core.buffer.Buffer;
  14 +import io.vertx.core.net.NetClient;
  15 +import io.vertx.core.net.NetClientOptions;
  16 +import io.vertx.core.net.NetSocket;
  17 +import io.vertx.core.parsetools.RecordParser;
  18 +import lombok.extern.slf4j.Slf4j;
  19 +import org.junit.jupiter.api.AfterAll;
  20 +import org.junit.jupiter.api.BeforeAll;
  21 +import org.junit.jupiter.api.Disabled;
  22 +import org.junit.jupiter.api.Test;
  23 +
  24 +import java.nio.ByteBuffer;
  25 +import java.nio.ByteOrder;
  26 +import java.util.concurrent.CompletableFuture;
  27 +import java.util.concurrent.TimeUnit;
  28 +
  29 +import static org.junit.jupiter.api.Assertions.assertEquals;
  30 +
  31 +/**
  32 + * IoT Modbus TCP Server 协议集成测试 — MODBUS_TCP 帧格式(手动测试)
  33 + *
  34 + * <p>测试场景:设备(TCP Client)连接到网关(TCP Server),使用 MODBUS_TCP(MBAP 头)帧格式通信
  35 + *
  36 + * <p>使用步骤:
  37 + * <ol>
  38 + * <li>启动 yudao-module-iot-gateway 服务(需开启 modbus-tcp-server 协议,默认端口 503)</li>
  39 + * <li>确保数据库有对应的 Modbus 设备配置(mode=1, frameFormat=modbus_tcp)</li>
  40 + * <li>运行以下测试方法:
  41 + * <ul>
  42 + * <li>{@link #testAuth()} - 自定义功能码认证</li>
  43 + * <li>{@link #testPollingResponse()} - 轮询响应</li>
  44 + * <li>{@link #testPropertySetWrite()} - 属性设置(接收写指令)</li>
  45 + * </ul>
  46 + * </li>
  47 + * </ol>
  48 + *
  49 + * @author 芋道源码
  50 + */
  51 +@Slf4j
  52 +@Disabled
  53 +public class IotModbusTcpServerTcpIntegrationTest {
  54 +
  55 + private static final String SERVER_HOST = "127.0.0.1";
  56 + private static final int SERVER_PORT = 503;
  57 + private static final int TIMEOUT_MS = 5000;
  58 +
  59 + private static final int CUSTOM_FC = 65;
  60 + private static final int SLAVE_ID = 1;
  61 +
  62 + private static Vertx vertx;
  63 + private static NetClient netClient;
  64 +
  65 + // ===================== 编解码器 =====================
  66 +
  67 + private static final IotModbusFrameDecoder FRAME_DECODER = new IotModbusFrameDecoder(CUSTOM_FC);
  68 + private static final IotModbusFrameEncoder FRAME_ENCODER = new IotModbusFrameEncoder(CUSTOM_FC);
  69 +
  70 + // ===================== 设备信息(根据实际情况修改,从 iot_device 表查询) =====================
  71 +
  72 + private static final String PRODUCT_KEY = "modbus_tcp_server_product_demo";
  73 + private static final String DEVICE_NAME = "modbus_tcp_server_device_demo_tcp";
  74 + private static final String DEVICE_SECRET = "8e4adeb3d25342ab88643421d3fba3f6";
  75 +
  76 + @BeforeAll
  77 + static void setUp() {
  78 + vertx = Vertx.vertx();
  79 + NetClientOptions options = new NetClientOptions()
  80 + .setConnectTimeout(TIMEOUT_MS)
  81 + .setIdleTimeout(TIMEOUT_MS);
  82 + netClient = vertx.createNetClient(options);
  83 + }
  84 +
  85 + @AfterAll
  86 + static void tearDown() {
  87 + if (netClient != null) {
  88 + netClient.close();
  89 + }
  90 + if (vertx != null) {
  91 + vertx.close();
  92 + }
  93 + }
  94 +
  95 + // ===================== 认证测试 =====================
  96 +
  97 + /**
  98 + * 认证测试:发送自定义功能码 FC65 认证帧,验证认证成功响应
  99 + */
  100 + @Test
  101 + public void testAuth() throws Exception {
  102 + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
  103 + try {
  104 + // 1. 构造并发送认证帧
  105 + IotModbusFrame response = authenticate(socket);
  106 +
  107 + // 2. 验证响应
  108 + log.info("[testAuth][认证响应帧: slaveId={}, FC={}, customData={}]",
  109 + response.getSlaveId(), response.getFunctionCode(), response.getCustomData());
  110 + JSONObject json = JSONUtil.parseObj(response.getCustomData());
  111 + assertEquals(0, json.getInt("code"));
  112 + log.info("[testAuth][认证结果: code={}, message={}]", json.getInt("code"), json.getStr("message"));
  113 + } finally {
  114 + socket.close();
  115 + }
  116 + }
  117 +
  118 + // ===================== 轮询响应测试 =====================
  119 +
  120 + /**
  121 + * 轮询响应测试:认证后持续监听网关下发的读请求,每次收到都自动构造读响应帧发回
  122 + */
  123 + @Test
  124 + public void testPollingResponse() throws Exception {
  125 + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
  126 + try {
  127 + // 1. 先认证
  128 + IotModbusFrame authResponse = authenticate(socket);
  129 + log.info("[testPollingResponse][认证响应: {}]", authResponse.getCustomData());
  130 + JSONObject authJson = JSONUtil.parseObj(authResponse.getCustomData());
  131 + assertEquals(0, authJson.getInt("code"));
  132 +
  133 + // 2. 设置持续监听:每收到一个读请求,自动回复
  134 + log.info("[testPollingResponse][开始持续监听网关下发的读请求...]");
  135 + RecordParser parser = FRAME_DECODER.createRecordParser((frame, frameFormat) -> {
  136 + log.info("[testPollingResponse][收到请求: slaveId={}, FC={}, transactionId={}]",
  137 + frame.getSlaveId(), frame.getFunctionCode(), frame.getTransactionId());
  138 + // 解析读请求中的起始地址和数量
  139 + byte[] pdu = frame.getPdu();
  140 + int startAddress = ((pdu[0] & 0xFF) << 8) | (pdu[1] & 0xFF);
  141 + int quantity = ((pdu[2] & 0xFF) << 8) | (pdu[3] & 0xFF);
  142 + log.info("[testPollingResponse][读请求参数: startAddress={}, quantity={}]", startAddress, quantity);
  143 +
  144 + // 构造读响应帧(模拟寄存器数据)
  145 + int[] registerValues = new int[quantity];
  146 + for (int i = 0; i < quantity; i++) {
  147 + registerValues[i] = 100 + i * 100; // 模拟值: 100, 200, 300, ...
  148 + }
  149 + byte[] responseData = buildReadResponse(frame.getTransactionId(),
  150 + frame.getSlaveId(), frame.getFunctionCode(), registerValues);
  151 + socket.write(Buffer.buffer(responseData));
  152 + log.info("[testPollingResponse][已发送读响应, registerValues={}]", registerValues);
  153 + });
  154 + socket.handler(parser);
  155 +
  156 + // 3. 持续等待(200 秒),期间会自动回复所有收到的读请求
  157 + Thread.sleep(200000);
  158 + } finally {
  159 + socket.close();
  160 + }
  161 + }
  162 +
  163 + // ===================== 属性设置测试 =====================
  164 +
  165 + /**
  166 + * 属性设置测试:认证后等待接收网关下发的 FC06/FC16 写请求
  167 + * <p>
  168 + * 注意:需手动在平台触发 property.set
  169 + */
  170 + @Test
  171 + public void testPropertySetWrite() throws Exception {
  172 + NetSocket socket = connect().get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
  173 + try {
  174 + // 1. 先认证
  175 + IotModbusFrame authResponse = authenticate(socket);
  176 + log.info("[testPropertySetWrite][认证响应: {}]", authResponse.getCustomData());
  177 +
  178 + // 2. 等待网关下发写请求(需手动在平台触发 property.set)
  179 + log.info("[testPropertySetWrite][等待网关下发写请求(请在平台触发 property.set)...]");
  180 + IotModbusFrame writeRequest = waitForRequest(socket);
  181 + log.info("[testPropertySetWrite][收到写请求: slaveId={}, FC={}, transactionId={}, pdu={}]",
  182 + writeRequest.getSlaveId(), writeRequest.getFunctionCode(),
  183 + writeRequest.getTransactionId(), HexUtil.encodeHexStr(writeRequest.getPdu()));
  184 + } finally {
  185 + socket.close();
  186 + }
  187 + }
  188 +
  189 + // ===================== 辅助方法 =====================
  190 +
  191 + /**
  192 + * 建立 TCP 连接
  193 + */
  194 + private CompletableFuture<NetSocket> connect() {
  195 + CompletableFuture<NetSocket> future = new CompletableFuture<>();
  196 + netClient.connect(SERVER_PORT, SERVER_HOST)
  197 + .onSuccess(future::complete)
  198 + .onFailure(future::completeExceptionally);
  199 + return future;
  200 + }
  201 +
  202 + /**
  203 + * 执行认证并返回响应帧
  204 + */
  205 + private IotModbusFrame authenticate(NetSocket socket) throws Exception {
  206 + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(PRODUCT_KEY, DEVICE_NAME, DEVICE_SECRET);
  207 + authInfo.setClientId(""); // 特殊:考虑到 modbus 消息长度限制,默认 clientId 不发送
  208 + byte[] authFrame = buildAuthFrame(authInfo.getClientId(), authInfo.getUsername(), authInfo.getPassword());
  209 + return sendAndReceive(socket, authFrame);
  210 + }
  211 +
  212 + /**
  213 + * 发送帧并等待响应(使用 IotModbusFrameDecoder 自动检测帧格式并解码)
  214 + */
  215 + private IotModbusFrame sendAndReceive(NetSocket socket, byte[] frameData) throws Exception {
  216 + CompletableFuture<IotModbusFrame> responseFuture = new CompletableFuture<>();
  217 + // 使用 FrameDecoder 创建拆包器(自动检测帧格式 + 解码,直接回调 IotModbusFrame)
  218 + RecordParser parser = FRAME_DECODER.createRecordParser(
  219 + (frame, frameFormat) -> {
  220 + try {
  221 + log.info("[sendAndReceive][检测到帧格式: {}]", frameFormat);
  222 + responseFuture.complete(frame);
  223 + } catch (Exception e) {
  224 + responseFuture.completeExceptionally(e);
  225 + }
  226 + });
  227 + socket.handler(parser);
  228 +
  229 + // 发送请求
  230 + log.info("[sendAndReceive][发送帧, 长度={}]", frameData.length);
  231 + socket.write(Buffer.buffer(frameData));
  232 +
  233 + // 等待响应
  234 + return responseFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
  235 + }
  236 +
  237 + /**
  238 + * 等待接收网关下发的请求帧(不发送,只等待接收)
  239 + */
  240 + private IotModbusFrame waitForRequest(NetSocket socket) throws Exception {
  241 + CompletableFuture<IotModbusFrame> requestFuture = new CompletableFuture<>();
  242 + // 使用 FrameDecoder 创建拆包器(直接回调 IotModbusFrame)
  243 + RecordParser parser = FRAME_DECODER.createRecordParser(
  244 + (frame, frameFormat) -> {
  245 + try {
  246 + log.info("[waitForRequest][检测到帧格式: {}]", frameFormat);
  247 + requestFuture.complete(frame);
  248 + } catch (Exception e) {
  249 + requestFuture.completeExceptionally(e);
  250 + }
  251 + });
  252 + socket.handler(parser);
  253 +
  254 + // 等待(超时 30 秒,因为轮询间隔可能比较长)
  255 + return requestFuture.get(30000, TimeUnit.MILLISECONDS);
  256 + }
  257 +
  258 + /**
  259 + * 构造认证帧(MODBUS_TCP 格式)
  260 + * <p>
  261 + * JSON: {"method":"auth","params":{"clientId":"...","username":"...","password":"..."}}
  262 + */
  263 + private byte[] buildAuthFrame(String clientId, String username, String password) {
  264 + JSONObject params = new JSONObject();
  265 + params.set("clientId", clientId);
  266 + params.set("username", username);
  267 + params.set("password", password);
  268 + JSONObject json = new JSONObject();
  269 + json.set("method", "auth");
  270 + json.set("params", params);
  271 + return FRAME_ENCODER.encodeCustomFrame(SLAVE_ID, json.toString(),
  272 + IotModbusFrameFormatEnum.MODBUS_TCP, 1);
  273 + }
  274 +
  275 + /**
  276 + * 构造 FC03/FC01-04 读响应帧(MODBUS_TCP 格式)
  277 + * <p>
  278 + * 格式:[MBAP(6)] [UnitId(1)] [FC(1)] [ByteCount(1)] [RegisterData(N*2)]
  279 + */
  280 + private byte[] buildReadResponse(int transactionId, int slaveId, int functionCode, int[] registerValues) {
  281 + int byteCount = registerValues.length * 2;
  282 + // PDU: FC(1) + ByteCount(1) + Data(N*2)
  283 + int pduLength = 1 + 1 + byteCount;
  284 + // 完整帧:MBAP(6) + UnitId(1) + PDU
  285 + int totalLength = 6 + 1 + pduLength;
  286 + ByteBuffer buf = ByteBuffer.allocate(totalLength).order(ByteOrder.BIG_ENDIAN);
  287 + // MBAP Header
  288 + buf.putShort((short) transactionId); // Transaction ID
  289 + buf.putShort((short) 0); // Protocol ID
  290 + buf.putShort((short) (1 + pduLength)); // Length (UnitId + PDU)
  291 + // UnitId
  292 + buf.put((byte) slaveId);
  293 + // PDU
  294 + buf.put((byte) functionCode);
  295 + buf.put((byte) byteCount);
  296 + for (int value : registerValues) {
  297 + buf.putShort((short) value);
  298 + }
  299 + return buf.array();
  300 + }
  301 +
  302 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java
... ... @@ -239,12 +239,28 @@ public class IotDirectDeviceMqttProtocolIntegrationTest {
239 239 log.info("[testSubscribe][连接认证成功]");
240 240  
241 241 try {
242   - // 2. 设置消息处理器
243   - client.publishHandler(message -> log.info("[testSubscribe][收到消息: topic={}, payload={}]",
244   - message.topicName(), message.payload().toString()));
  242 + // 2. 设置消息处理器:收到属性设置时,回复 _reply 消息
  243 + client.publishHandler(message -> {
  244 + log.info("[testSubscribe][收到消息: topic={}, payload={}]",
  245 + message.topicName(), message.payload().toString());
  246 + // 收到属性设置消息时,回复 _reply
  247 + if (message.topicName().endsWith("/thing/property/set")) {
  248 + try {
  249 + IotDeviceMessage received = SERIALIZER.deserialize(message.payload().getBytes());
  250 + IotDeviceMessage reply = IotDeviceMessage.replyOf(
  251 + received.getRequestId(), "thing.property.set_reply", null, 0, null);
  252 + String replyTopic = String.format("/sys/%s/%s/thing/property/set_reply", PRODUCT_KEY, DEVICE_NAME);
  253 + byte[] replyPayload = SERIALIZER.serialize(reply);
  254 + client.publish(replyTopic, Buffer.buffer(replyPayload), MqttQoS.AT_LEAST_ONCE, false, false);
  255 + log.info("[testSubscribe][已回复属性设置: topic={}]", replyTopic);
  256 + } catch (Exception e) {
  257 + log.error("[testSubscribe][回复属性设置异常]", e);
  258 + }
  259 + }
  260 + });
245 261  
246   - // 3. 订阅下行主题
247   - String topic = String.format("/sys/%s/%s/thing/service/#", PRODUCT_KEY, DEVICE_NAME);
  262 + // 3. 订阅下行主题(属性设置 + 服务调用)
  263 + String topic = String.format("/sys/%s/%s/#", PRODUCT_KEY, DEVICE_NAME);
248 264 log.info("[testSubscribe][订阅主题: {}]", topic);
249 265 subscribe(client, topic);
250 266 log.info("[testSubscribe][订阅成功,等待下行消息... (30秒后自动断开)]");
... ...
yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/service/account/MpAccountServiceImpl.java
... ... @@ -60,7 +60,7 @@ public class MpAccountServiceImpl implements MpAccountService {
60 60 private MpAccountMapper mpAccountMapper;
61 61  
62 62 @Resource
63   - @Lazy // 延迟加载,解决循环依赖的问题
  63 + @Lazy // 延迟加载,解决循环依赖
64 64 private MpServiceFactory mpServiceFactory;
65 65  
66 66 @Override
... ...